diff --git a/README.md b/README.md deleted file mode 100644 index 6f8a176c..00000000 --- a/README.md +++ /dev/null @@ -1 +0,0 @@ -# pulp_template \ No newline at end of file diff --git a/README.rst b/README.rst new file mode 100644 index 00000000..a4e94f13 --- /dev/null +++ b/README.rst @@ -0,0 +1,120 @@ +Tepmlate to create your own plugin +================================== + +This is the ``plugin_template`` repository to help plugin writers +get started and write their own plugin for `Pulp Project +3.0+ `__. + +Clone this repository and run the provided ``rename.py`` script to create +a skeleton for your plugin with the name of your choice. It will contain +``setup.py``, expected plugin layout and stubs for necessary classes and methods. + +``$ git clone https://github.com/pulp/plugin_template.git`` + +``$ cd plugin_template`` + +``$ ./rename.py your_plugin_name`` + +Check `Plugin Writer's Guide `__ +for more details and suggestions on plugin implementaion. + +Below are some ideas for how to document your plugin. + + +All REST API examples below use `httpie `__ to +perform the requests. + +Install ``pulpcore`` +-------------------- + +Follow the `installation +instructions `__ +provided with pulpcore. + +Install plugin +-------------- + +From source +~~~~~~~~~~~ + +Define installation steps here. + +Install from PyPI +~~~~~~~~~~~~~~~~~ + +Define installation steps here. + + +Create a repository ``foo`` +--------------------------- + +``$ http POST http://localhost:8000/api/v3/repositories/ name=foo`` + +Add an Importer to repository ``foo`` +------------------------------------- + +Add important details about your Importer and provide examples. + +``$ http POST http://localhost:8000/api/v3/repositories/foo/importers/plugin-template/ some=params`` + +.. code:: json + + { + "_href": "http://localhost:8000/api/v3/repositories/foo/importers/plugin-template/bar/", + ... + } + +Add a Publisher to repository ``foo`` +------------------------------------- + +``$ http POST http://localhost:8000/api/v3/repositories/foo/publishers/plugin-template/ name=bar`` + +.. code:: json + + { + "_href": "http://localhost:8000/api/v3/repositories/foo/publishers/plugin-template/bar/", + ... + } + +Add a Distribution to Publisher ``bar`` +--------------------------------------- + +``$ http POST http://localhost:8000/api/v3/repositories/foo/publishers/plugin-template/bar/distributions/ some=params`` + +Sync repository ``foo`` using Importer ``bar`` +---------------------------------------------- + +Use ``plugin-template`` Importer: + +``http POST http://localhost:8000/api/v3/repositories/foo/importers/plugin-template/bar/sync/`` + +Add content to repository ``foo`` +--------------------------------- + +``$ http POST http://localhost:8000/api/v3/repositorycontents/ repository='http://localhost:8000/api/v3/repositories/foo/' content='http://localhost:8000/api/v3/content/plugin-template/a9578a5f-c59f-4920-9497-8d1699c112ff/'`` + +Create a Publication using Publisher ``bar`` +-------------------------------------------- + +Dispatch the Publish task + +``$ http POST http://localhost:8000/api/v3/repositories/foo/publishers/plugin-template/bar/publish/`` + +.. code:: json + + [ + { + "_href": "http://localhost:8000/api/v3/tasks/fd4cbecd-6c6a-4197-9cbe-4e45b0516309/", + "task_id": "fd4cbecd-6c6a-4197-9cbe-4e45b0516309" + } + ] + +Check status of a task +---------------------- + +``$ http GET http://localhost:8000/api/v3/tasks/82e64412-47f8-4dd4-aa55-9de89a6c549b/`` + +Download ``foo.tar.gz`` from Pulp +--------------------------------- + +``$ http GET http://localhost:8000/content/foo/foo.tar.gz`` diff --git a/flake8.cfg b/flake8.cfg new file mode 100644 index 00000000..d65b11a1 --- /dev/null +++ b/flake8.cfg @@ -0,0 +1,5 @@ +[flake8] +exclude = */migrations/* +# E401: multiple imports on one line +ignore = E401 +max-line-length = 100 \ No newline at end of file diff --git a/pulp_plugin_template/__init__.py b/pulp_plugin_template/__init__.py new file mode 100644 index 00000000..20cc86a1 --- /dev/null +++ b/pulp_plugin_template/__init__.py @@ -0,0 +1 @@ +default_app_config = 'pulp_plugin_template.app.PulpPluginTemplatePluginAppConfig' diff --git a/pulp_plugin_template/app/__init__.py b/pulp_plugin_template/app/__init__.py new file mode 100644 index 00000000..69500f6a --- /dev/null +++ b/pulp_plugin_template/app/__init__.py @@ -0,0 +1,6 @@ +from pulpcore.plugin import PulpPluginAppConfig + + +class PulpPluginTemplatePluginAppConfig(PulpPluginAppConfig): + name = 'pulp_plugin_template.app' + label = 'pulp_plugin_template' diff --git a/pulp_plugin_template/app/models.py b/pulp_plugin_template/app/models.py new file mode 100644 index 00000000..438b1d9e --- /dev/null +++ b/pulp_plugin_template/app/models.py @@ -0,0 +1,84 @@ +""" +Check `Plugin Writer's Guide`_ and `pulp_example`_ plugin +implementation for more details. + +.. _Plugin Writer's Guide: + http://docs.pulpproject.org/en/3.0/nightly/plugins/plugin-writer/index.html + +.. _pulp_example: + https://github.com/pulp/pulp_example/ +""" + +from gettext import gettext as _ +from logging import getLogger + +from django.db import models + +from pulpcore.plugin.models import (Artifact, Content, ContentArtifact, RemoteArtifact, Importer, + ProgressBar, Publisher, RepositoryContent, PublishedArtifact, + PublishedMetadata) +from pulpcore.plugin.tasking import Task + + +log = getLogger(__name__) + + +class PluginTemplateContent(Content): + """ + The "plugin-template" content type. + + Define fields you need for your new content type and + specify uniqueness constraint to identify unit of this type. + + For example:: + + field1 = models.TextField() + field2 = models.IntegerField() + field3 = models.CharField() + + class Meta: + unique_together = (field1, field2) + """ + TYPE = 'plugin-template' + + @classmethod + def natural_key_fields(cls): + for unique in cls._meta.unique_together: + for field in unique: + yield field + + +class PluginTemplatePublisher(Publisher): + """ + A Publisher for PluginTemplateContent. + + Define any additional fields for your new publisher if needed. + A ``publish`` method should be defined. + It is responsible for publishing metadata and artifacts + which belongs to a specific repository. + """ + TYPE = 'plugin-template' + + def publish(self): + """ + Publish the repository. + """ + raise NotImplementedError + + +class PluginTemplateImporter(Importer): + """ + An Importer for PluginTemplateContent. + + Define any additional fields for your new importer if needed. + A ``sync`` method should be defined. + It is responsible for parsing metadata of the content, + downloading of the content and saving it to Pulp. + """ + TYPE = 'plugin-template' + + def sync(self): + """ + Synchronize the repository with the remote repository. + """ + raise NotImplementedError diff --git a/pulp_plugin_template/app/serializers.py b/pulp_plugin_template/app/serializers.py new file mode 100644 index 00000000..c0a86b8f --- /dev/null +++ b/pulp_plugin_template/app/serializers.py @@ -0,0 +1,61 @@ +""" +Check `Plugin Writer's Guide`_ and `pulp_example`_ plugin +implementation for more details. + +.. _Plugin Writer's Guide: + http://docs.pulpproject.org/en/3.0/nightly/plugins/plugin-writer/index.html + +.. _pulp_example: + https://github.com/pulp/pulp_example/ +""" + +from rest_framework import serializers +from pulpcore.plugin import serializers as platform + +from . import models + + +class PluginTemplateContentSerializer(platform.ContentSerializer): + """ + A Serializer for PluginTemplateContent. + + Add serializers for the new fields defined in PluginTemplateContent and + add those fields to the Meta class keeping fields from the parent class as well. + + For example:: + + field1 = serializers.TextField() + field2 = serializers.IntegerField() + field3 = serializers.CharField() + + class Meta: + fields = platform.ContentSerializer.Meta.fields + ('field1', 'field2', 'field3') + model = models.PluginTemplateContent + """ + class Meta: + fields = platform.ContentSerializer.Meta.fields + model = models.PluginTemplateContent + + +class PluginTemplateImporterSerializer(platform.ImporterSerializer): + """ + A Serializer for PluginTemplateImporter. + + Add any new fields if defined on PluginTemplateImporter. + Similar to the example above, in PluginTemplateContentSerializer. + """ + class Meta: + fields = platform.ImporterSerializer.Meta.fields + model = models.PluginTemplateImporter + + +class PluginTemplatePublisherSerializer(platform.PublisherSerializer): + """ + A Serializer for PluginTemplatePublisher. + + Add any new fields if defined on PluginTemplatePublisher. + Similar to the example above, in PluginTemplateContentSerializer. + """ + class Meta: + fields = platform.PublisherSerializer.Meta.fields + model = models.PluginTemplatePublisher diff --git a/pulp_plugin_template/app/viewsets.py b/pulp_plugin_template/app/viewsets.py new file mode 100644 index 00000000..735eb2b7 --- /dev/null +++ b/pulp_plugin_template/app/viewsets.py @@ -0,0 +1,53 @@ +""" +Check `Plugin Writer's Guide`_ and `pulp_example`_ plugin +implementation for more details. + +.. _Plugin Writer's Guide: + http://docs.pulpproject.org/en/3.0/nightly/plugins/plugin-writer/index.html + +.. _pulp_example: + https://github.com/pulp/pulp_example/ +""" + +from pulpcore.plugin import viewsets as platform + +from . import models, serializers + + +class PluginTemplateContentViewSet(platform.ContentViewSet): + """ + A ViewSet for PluginTemplateContent. + + Define endpoint name which will appear in the API endpoint for this content type. + For example:: + http://pulp.example.com/api/v3/content/plugin-template/ + + Also specify queryset and serializer for PluginTemplateContent. + """ + endpoint_name = 'plugin-template' + queryset = models.PluginTemplateContent.objects.all() + serializer_class = serializers.PluginTemplateContentSerializer + + +class PluginTemplateImporterViewSet(platform.ImporterViewSet): + """ + A ViewSet for PluginTemplateImporter. + + Similar to the PluginTemplateContentViewSet above, define endpoint_name, + queryset and serializer, at a minimum. + """ + endpoint_name = 'plugin-template' + queryset = models.PluginTemplateImporter.objects.all() + serializer_class = serializers.PluginTemplateImporterSerializer + + +class PluginTemplatePublisherViewSet(platform.PublisherViewSet): + """ + A ViewSet for PluginTemplatePublisher. + + Similar to the PluginTemplateContentViewSet above, define endpoint_name, + queryset and serializer, at a minimum. + """ + endpoint_name = 'plugin-template' + queryset = models.PluginTemplatePublisher.objects.all() + serializer_class = serializers.PluginTemplatePublisherSerializer diff --git a/rename.py b/rename.py new file mode 100755 index 00000000..1e4c3a30 --- /dev/null +++ b/rename.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 + +import argparse +import os +import re +import shutil +import sys +import tempfile +import textwrap + +TEMPLATE_SNAKE = 'pulp_plugin_template' +TEMPLATE_CAMEL = 'PulpPluginTemplate' +TEMPLATE_CAMEL_SHORT = 'PluginTemplate' +TEMPLATE_DASH = 'pulp-plugin-template' +TEMPLATE_DASH_SHORT = 'plugin-template' +IGNORE_FILES = ('LICENSE', 'rename.py', 'flake8.cfg') +IGNORE_COPYTREE = ('.git*', '*.pyc', '*.egg-info', 'rename.py', '__pycache__') + + +def is_valid(name): + """ + Check if specified name is compliant with requirements for it. + + The max length of the name is 16 characters. It seems reasonable to have this limitation + because the plugin name is used for directory name on the file system and it is also used + as a name of some Python objects, like class names, so it is expected to be relatively short. + """ + return bool(re.match(r'^[a-z][0-9a-z_]{2,15}$', name)) + + +def to_camel(name): + """ + Convert plugin name from snake to camel case + """ + return name.title().replace('_', '') + + +def to_dash(name): + """ + Convert plugin name from snake case to dash representation + """ + return name.replace('_', '-') + + +def main(): + parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter, + description='rename template data to a specified plugin name') + parser.add_argument('plugin_name', type=str, + help=textwrap.dedent('''\ + set plugin name to this one + + Requirements for plugin name: + - specified in the snake form: your_new_plugin_name + - consists of 3-16 characters + - possible characters are letters [a-z], numbers [0-9], underscore [_] + - first character should be a letter [a-z] + ''')) + args = parser.parse_args() + plugin_name = args.plugin_name + + if not is_valid(plugin_name): + parser.print_help() + return 2 + + pulp_plugin_name = 'pulp_' + plugin_name + replace_map = {TEMPLATE_SNAKE: pulp_plugin_name, + TEMPLATE_DASH_SHORT: to_dash(plugin_name), + TEMPLATE_DASH: to_dash(pulp_plugin_name), + TEMPLATE_CAMEL_SHORT: to_camel(plugin_name), + TEMPLATE_CAMEL: to_camel(pulp_plugin_name)} + + # copy template directory + orig_root_dir = os.path.dirname(os.path.abspath(parser.prog)) + dst_root_dir = os.path.join(os.path.dirname(orig_root_dir), pulp_plugin_name) + try: + shutil.copytree(orig_root_dir, dst_root_dir, + ignore=shutil.ignore_patterns(*IGNORE_COPYTREE)) + except FileExistsError: + print(textwrap.dedent(''' + It looks like plugin with such name already exists! + Please, choose another name. + ''')) + return 1 + + # rename python package directory + listed_dir = os.listdir(dst_root_dir) + if TEMPLATE_SNAKE in listed_dir: + os.rename(os.path.join(dst_root_dir, TEMPLATE_SNAKE), + os.path.join(dst_root_dir, pulp_plugin_name)) + + # replace text + for dir_path, dirs, files in os.walk(dst_root_dir): + for file in files: + # skip files which don't need any text replacement + if file in IGNORE_FILES: + continue + + file_path = os.path.join(dir_path, file) + + # write substituted text to temporary file + with open(file_path) as fd_in, tempfile.NamedTemporaryFile(mode='w', dir=dir_path, + delete=False) as fd_out: + tempfile_path = fd_out.name + text = fd_in.read() + for old, new in replace_map.items(): + text = text.replace(old, new) + fd_out.write(text) + + # overwrite existing file by renaming the temporary one + os.rename(tempfile_path, file_path) + + return 0 + +if __name__ == '__main__': + sys.exit(main()) diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..49c3baf9 --- /dev/null +++ b/setup.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 + +from setuptools import setup + +requirements = [ + 'pulpcore-plugin', +] + +setup( + name='pulp-plugin-template', + version='0.0.1a1.dev1', + description='pulp-plugin-template plugin for the Pulp Project', + author='AUTHOR', + author_email='author@email.here', + url='http://example.com/', + install_requires=requirements, + include_package_data=True, + packages=['pulp_plugin_template', 'pulp_plugin_template.app'], + entry_points={ + 'pulpcore.plugin': [ + 'pulp_plugin_template = pulp_plugin_template:default_app_config', + ] + } +)