-
Notifications
You must be signed in to change notification settings - Fork 86
Added twine upload support #367
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Added ``twine`` (and other similar Python tools) package upload support |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| build | ||
| coreapi | ||
| django | ||
| djangorestframework | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| pulp python distribution create --name my-pypi --base-path my-pypi --repository foo |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| # Build custom package | ||
| python -m build $PLUGIN_SOURCE | ||
| # Upload built package distributions to my-pypi | ||
| twine upload --repository-url $BASE_ADDR/pypi/my-pypi/simple/ -u admin -p password "$PLUGIN_SOURCE"dist/* |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,6 +24,7 @@ Pulp CLI documentation on how to do that. | |
| .. toctree:: | ||
| :maxdepth: 2 | ||
|
|
||
| pypi | ||
| sync | ||
| upload | ||
| publish | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| .. _pypi-workflow: | ||
|
|
||
| Setup your own PyPI: | ||
| ==================== | ||
|
|
||
| This section guides you through the quickest way to to setup ``pulp_python`` to act as your very own | ||
| private ``PyPI``. | ||
|
|
||
| Create a Repository: | ||
| -------------------- | ||
|
|
||
| Repositories are the base objects ``Pulp`` uses to store and organize its content. They are automatically | ||
| versioned when content is added or deleted and allow for easy rollbacks to previous versions. | ||
|
|
||
| .. literalinclude:: ../_scripts/repo.sh | ||
| :language: bash | ||
|
|
||
| Repository Create Response:: | ||
|
|
||
| { | ||
| "pulp_href": "/pulp/api/v3/repositories/python/python/3fe0d204-217f-4250-8177-c83b30751fca/", | ||
| "pulp_created": "2021-06-02T14:54:53.387054Z", | ||
| "versions_href": "/pulp/api/v3/repositories/python/python/3fe0d204-217f-4250-8177-c83b30751fca/versions/", | ||
| "pulp_labels": {}, | ||
| "latest_version_href": "/pulp/api/v3/repositories/python/python/3fe0d204-217f-4250-8177-c83b30751fca/versions/1/", | ||
| "name": "foo", | ||
| "description": null, | ||
| "retained_versions": null, | ||
| "remote": null, | ||
| "autopublish": false | ||
| } | ||
|
|
||
| Create a Distribution: | ||
| ---------------------- | ||
|
|
||
| Distributions serve the content stored in repositories so that it can be used by tools like ``pip``. | ||
|
|
||
| .. literalinclude:: ../_scripts/index.sh | ||
| :language: bash | ||
|
|
||
| Distribution Create Response:: | ||
|
|
||
| { | ||
| "pulp_href": "/pulp/api/v3/distributions/python/pypi/e8438593-fd40-4654-8577-65398833c331/", | ||
| "pulp_created": "2021-06-03T20:04:18.233230Z", | ||
| "base_path": "my-pypi", | ||
| "base_url": "https://pulp3-source-fedora33.localhost.example.com/pypi/foo/", | ||
| "content_guard": null, | ||
| "pulp_labels": {}, | ||
| "name": "my-pypi", | ||
| "repository": "/pulp/api/v3/repositories/python/python/3fe0d204-217f-4250-8177-c83b30751fca/", | ||
| "publication": null, | ||
| "allow_uploads": true | ||
| } | ||
|
|
||
| Upload and Install Packages: | ||
| ---------------------------- | ||
|
|
||
| Packages can now be uploaded to the index using your favorite Python tool. The index url will be available | ||
| at ``/pypi/<distribution.base_path>/simple/``. | ||
|
|
||
| .. literalinclude:: ../_scripts/twine.sh | ||
| :language: bash | ||
|
|
||
| Packages can then be installed using your favorite Python tool:: | ||
|
|
||
| $ pip install --trusted-host localhost -i $BASE_ADDR/pypi/my-pypi/simple/ shelf-reader | ||
|
|
||
| Now you have a fully operational Python package index. Check out the other workflows to see more features of | ||
| ``pulp_python``. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| git+https://github.com/pulp/pulp-smash.git#egg=pulp-smash | ||
| pytest | ||
| lxml | ||
| twine |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| # Generated by Django 2.2.24 on 2021-06-10 14:50 | ||
|
|
||
| from django.db import migrations, models | ||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
|
|
||
| dependencies = [ | ||
| ('python', '0008_pythonpackagecontent_unique_sha256'), | ||
| ] | ||
|
|
||
| operations = [ | ||
| migrations.AddField( | ||
| model_name='pythondistribution', | ||
| name='allow_uploads', | ||
| field=models.BooleanField(default=True), | ||
| ), | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -43,6 +43,8 @@ class PythonDistribution(Distribution): | |
|
|
||
| TYPE = 'python' | ||
|
|
||
| allow_uploads = models.BooleanField(default=True) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It looks like the container registry plugin uses an entirely separate distribution type for this. I'm not sure what the reasons are, or if they would apply to the Python plugin as well. @ipanova could you weigh in?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Container registry uses same type of distribution, it however has 2 repo_types. Push repo type is auto-created during 'podman push', this is basically a read-only repo which you subsequently cannot use for mirroring.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's my understanding of the way this PR works currently. Are there security reasons for the way the container plugin does it?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. podman push operation creates a lot of intermediary repo-versions and only last repo-version is content compete meaning that in push-repo-type can be served only last version and it also means that rollback does not work. This however brings certain amount of complexity which we are figuring out how to minimize https://hackmd.io/z7i694VeTR-p4hgTk2Oocw#Food-for-thoughts
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So it sounds like the container plugin has to deal with inter-content dependencies, and therefore intermediate repository versions are broken -- but because Python repositories only have one type of content, and one upload per content, that we therefore don't need to follow what the container plugin does with regards to separate repository / distribution types. Thanks @ipanova, that is super helpful feedback. |
||
|
|
||
| def content_handler(self, path): | ||
| """ | ||
| Handler to serve extra, non-Artifact content for this Distribution | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,12 @@ | ||
| import logging | ||
| from gettext import gettext as _ | ||
|
|
||
| from rest_framework import serializers | ||
| from pulp_python.app.tasks.upload import DIST_EXTENSIONS | ||
| from pulpcore.plugin.models import Artifact | ||
| from django.db.utils import IntegrityError | ||
|
|
||
| log = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| class SummarySerializer(serializers.Serializer): | ||
|
|
@@ -9,7 +15,9 @@ class SummarySerializer(serializers.Serializer): | |
| """ | ||
|
|
||
| projects = serializers.IntegerField(help_text=_("Number of Python projects in index")) | ||
| releases = serializers.IntegerField(help_text=_("Number of Python distributions in index")) | ||
| releases = serializers.IntegerField( | ||
| help_text=_("Number of Python distribution releases in index") | ||
| ) | ||
| files = serializers.IntegerField(help_text=_("Number of files for all distributions in index")) | ||
|
|
||
|
|
||
|
|
@@ -22,3 +30,63 @@ class PackageMetadataSerializer(serializers.Serializer): | |
| info = serializers.JSONField(help_text=_("Core metadata of the package")) | ||
| releases = serializers.JSONField(help_text=_("List of all the releases of the package")) | ||
| urls = serializers.JSONField() | ||
|
|
||
|
|
||
| class PackageUploadSerializer(serializers.Serializer): | ||
| """ | ||
| A Serializer for Python packages being uploaded to the index. | ||
| """ | ||
|
|
||
| content = serializers.FileField( | ||
| help_text=_("A Python package release file to upload to the index."), | ||
| required=True, | ||
| write_only=True, | ||
| ) | ||
| action = serializers.CharField( | ||
| help_text=_("Defaults to `file_upload`, don't change it or request will fail!"), | ||
| default="file_upload", | ||
| source=":action" | ||
| ) | ||
| sha256_digest = serializers.CharField( | ||
| help_text=_("SHA256 of package to validate upload integrity."), | ||
| required=True, | ||
| min_length=64, | ||
| max_length=64, | ||
| ) | ||
|
|
||
| def validate(self, data): | ||
| """Validates the request.""" | ||
| action = data.get(":action") | ||
| if action != "file_upload": | ||
| raise serializers.ValidationError( | ||
| _("We do not support the :action {}").format(action) | ||
| ) | ||
| file = data.get("content") | ||
| for ext, packagetype in DIST_EXTENSIONS.items(): | ||
| if file.name.endswith(ext): | ||
| break | ||
| else: | ||
| raise serializers.ValidationError(_( | ||
| "Extension on {} is not a valid python extension " | ||
| "(.whl, .exe, .egg, .tar.gz, .tar.bz2, .zip)").format(file.name) | ||
| ) | ||
| sha256 = data.get("sha256_digest") | ||
| digests = {"sha256": sha256} if sha256 else None | ||
| artifact = Artifact.init_and_validate(file, expected_digests=digests) | ||
| try: | ||
| artifact.save() | ||
| except IntegrityError: | ||
| artifact = Artifact.objects.get(sha256=artifact.sha256) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is inconsistent with Warehouse. The standard PyPI returns 403 on duplicate artifacts.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, inconsistent with Warehouse, but not so for Pulp. One of Pulp's key features is artifact de-duplication. If an artifact is shared among multiple content or a piece of content is contained in multiple repositories, Pulp only stores the one artifact or content unit once. So for pulp python the content unit is the Python package and its artifact is the tarball or wheel itself. Since Pulp can have multiple repositories if the package is already present in Pulp, but not in the repository, then it should be treated as adding that package to that repository. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What if the artifact exists but the hash is different? It mustn't be possible to mutate the same dist. At least this case should error out.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
With the same hash or a different one?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Either or. Currently artifacts in Pulp are unique by their hash, but python packages in pulp_python are only unique by their filename. Since a goal of Pulp is to allow hosting multiple indexes, I think I'm going to need to change the global uniqueness constraint for python packages to be their hash, but still have the local uniqueness constraint for repositories be their filename.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've changed the uniqueness constraint for packages to be their sha256. If you upload a package with the same hash as the one already in Pulp, but not in the repository you will simply add that package to that repository. If you try to upload a package with the same name as another in Pulp but not in the repository and with a different hash then a new package will be created and added to the repository. This means there can be two packages with the same name but different digests in Pulp, however they both can't be in the same repository. The sha256 of the package's metadata and its artifact are now linked and can't be mismatched. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Awesome, this makes perfect sense 👍🏻 thx! |
||
| log.info(f"Artifact for {file.name} already existed in database") | ||
| data["content"] = (artifact, file.name) | ||
| return data | ||
|
|
||
|
|
||
| class PackageUploadTaskSerializer(serializers.Serializer): | ||
| """ | ||
| A Serializer for responding to a package upload task. | ||
| """ | ||
|
|
||
| session = serializers.CharField() | ||
| task = serializers.CharField() | ||
| task_start_time = serializers.DateTimeField() | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍