feat(pypi): add a standards compliant python_tag creator (#3110)

This will be needed when we start selecting wheels entirely in the
bzlmod extension evaluation phase (#3058).

This adds a few unit tests to just ensure that we conform to the spec
even though the code is very simple.

Work towards #2747
Work towards #2759
Work towards #2849
diff --git a/python/private/pypi/BUILD.bazel b/python/private/pypi/BUILD.bazel
index e5a916b..3a66170 100644
--- a/python/private/pypi/BUILD.bazel
+++ b/python/private/pypi/BUILD.bazel
@@ -337,6 +337,14 @@
 )
 
 bzl_library(
+    name = "python_tag_bzl",
+    srcs = ["python_tag.bzl"],
+    deps = [
+        "//python/private:version_bzl",
+    ],
+)
+
+bzl_library(
     name = "render_pkg_aliases_bzl",
     srcs = ["render_pkg_aliases.bzl"],
     deps = [
diff --git a/python/private/pypi/python_tag.bzl b/python/private/pypi/python_tag.bzl
new file mode 100644
index 0000000..224c5f9
--- /dev/null
+++ b/python/private/pypi/python_tag.bzl
@@ -0,0 +1,41 @@
+"A simple utility function to get the python_tag from the implementation name"
+
+load("//python/private:version.bzl", "version")
+
+# Taken from
+# https://packaging.python.org/en/latest/specifications/platform-compatibility-tags/#python-tag
+_PY_TAGS = {
+    # "py": Generic Python (does not require implementation-specific features)
+    "cpython": "cp",
+    "ironpython": "ip",
+    "jython": "jy",
+    "pypy": "pp",
+    "python": "py",
+}
+PY_TAG_GENERIC = "py"
+
+def python_tag(implementation_name, python_version = ""):
+    """Get the python_tag from the implementation_name.
+
+    Args:
+        implementation_name: {type}`str` the implementation name, e.g. "cpython"
+        python_version: {type}`str` a version who can be parsed using PEP440 compliant
+            parser.
+
+    Returns:
+        A {type}`str` that represents the python_tag with a version if the
+            python_version is given.
+    """
+    if python_version:
+        v = version.parse(python_version, strict = True)
+        suffix = "{}{}".format(
+            v.release[0],
+            v.release[1] if len(v.release) > 1 else "",
+        )
+    else:
+        suffix = ""
+
+    return "{}{}".format(
+        _PY_TAGS.get(implementation_name, implementation_name),
+        suffix,
+    )
diff --git a/tests/pypi/python_tag/BUILD.bazel b/tests/pypi/python_tag/BUILD.bazel
new file mode 100644
index 0000000..d4b37ce
--- /dev/null
+++ b/tests/pypi/python_tag/BUILD.bazel
@@ -0,0 +1,3 @@
+load(":python_tag_tests.bzl", "python_tag_test_suite")
+
+python_tag_test_suite(name = "python_tag_tests")
diff --git a/tests/pypi/python_tag/python_tag_tests.bzl b/tests/pypi/python_tag/python_tag_tests.bzl
new file mode 100644
index 0000000..ca86575
--- /dev/null
+++ b/tests/pypi/python_tag/python_tag_tests.bzl
@@ -0,0 +1,34 @@
+""
+
+load("@rules_testing//lib:test_suite.bzl", "test_suite")
+load("//python/private/pypi:python_tag.bzl", "python_tag")  # buildifier: disable=bzl-visibility
+
+_tests = []
+
+def _test_without_version(env):
+    for give, expect in {
+        "cpython": "cp",
+        "ironpython": "ip",
+        "jython": "jy",
+        "pypy": "pp",
+        "python": "py",
+        "something_else": "something_else",
+    }.items():
+        got = python_tag(give)
+        env.expect.that_str(got).equals(expect)
+
+_tests.append(_test_without_version)
+
+def _test_with_version(env):
+    got = python_tag("cpython", "3.1.15")
+    env.expect.that_str(got).equals("cp31")
+
+_tests.append(_test_with_version)
+
+def python_tag_test_suite(name):
+    """Create the test suite.
+
+    Args:
+        name: the name of the test suite
+    """
+    test_suite(name = name, basic_tests = _tests)