{"id":17972979,"url":"https://github.com/gi0baro/weppy-rest","last_synced_at":"2025-03-25T12:33:12.810Z","repository":{"id":62588714,"uuid":"60982111","full_name":"gi0baro/weppy-REST","owner":"gi0baro","description":"REST extension for weppy framework","archived":false,"fork":false,"pushed_at":"2019-03-11T08:55:16.000Z","size":36,"stargazers_count":15,"open_issues_count":2,"forks_count":2,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-03-20T08:44:53.628Z","etag":null,"topics":["rest","rest-api","weppy"],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/gi0baro.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2016-06-12T18:14:29.000Z","updated_at":"2023-03-27T20:33:16.000Z","dependencies_parsed_at":"2022-11-03T17:48:25.527Z","dependency_job_id":null,"html_url":"https://github.com/gi0baro/weppy-REST","commit_stats":null,"previous_names":[],"tags_count":11,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gi0baro%2Fweppy-REST","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gi0baro%2Fweppy-REST/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gi0baro%2Fweppy-REST/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gi0baro%2Fweppy-REST/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/gi0baro","download_url":"https://codeload.github.com/gi0baro/weppy-REST/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":245463030,"owners_count":20619600,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["rest","rest-api","weppy"],"created_at":"2024-10-29T16:26:45.792Z","updated_at":"2025-03-25T12:33:12.449Z","avatar_url":"https://github.com/gi0baro.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# weppy-REST\n\nweppy-REST is a REST extension for the [weppy framework](http://weppy.org).\n\n[![pip version](https://img.shields.io/pypi/v/weppy-rest.svg?style=flat)](https://pypi.python.org/pypi/weppy-REST) \n\n## Installation\n\nYou can install weppy-REST using pip:\n\n    pip install weppy-REST\n\nAnd add it to your weppy application:\n\n```python\nfrom weppy_rest import REST\n\napp.use_extension(REST)\n```\n\n## Usage\n\nThe weppy-REST extension is intended to be used with weppy models, and it uses application modules to build APIs over them. \n\nLet's say, for example that you have a task manager app, with a `Task` model like this:\n\n```python\nfrom weppy.orm import Model, Field\n\nclass Task(Model):\n    title = Field.string()\n    is_completed = Field.bool()\n    created_at = Field.datetime()\n```\n\nThen, in order to expose REST apis for your `Task` model, you can use the `rest_module` method on your application or on any application module:\n\n```python\nfrom myapp import app, Task\n\ntasks = app.rest_module(__name__, 'api_task', Task, url_prefix='tasks')\n```\n\nAs you can see, the usage is very similar to the weppy application modules, but we also passed the involved model to the module initialization.\n\nThis single line is enough to have a really simple REST api over the `Task` model, since under the default behaviour rest modules will expose 5 different routes:\n\n- an *index* route that will respond to `GET` requests on `/tasks` path listing all the tasks in the database\n- a *read* route that will respond to `GET` requests on `/tasks/\u003cint:rid\u003e` path returning a single task corresponding to the record id of the *rid* variable\n- a *create* route that will respond to `POST` requests on `/tasks` that will create a new task in the database\n- an *update* route that will respond to `PUT` or `PATCH` requests on `/tasks/\u003cint:rid\u003e` that will update the task corresponding to the record id of the *rid* variable\n- a *delete* route that will respond to `DELETE` requests on `/tasks/\u003cint:rid\u003e` that will delete the task corresponding to the record id of the *rid* variable.\n\n### REST module parameters\n\nThe `rest_module` method accepts several parameters (*bold ones are required*) for its configuration:\n\n| parameter | default | description |\n| --- | --- | --- |\n| **import_name** | | as for standard modules |\n| **name** | | as for standard modules |\n| **model** | | the model to use |\n| serializer | `None` | a class to be used for serialization |\n| parser | `None` | a class to be used for parsing |\n| enabled_methods | `str` list: index, create, read, update, delete | the routes that should be enabled on the module |\n| disabled_methods | `[]` | the routes that should be disabled on the module |\n| list_envelope | `'data'` | the envelope to use on the index route |\n| single_envelope | `None` | the envelope to use on all the routes except for index |\n| use\\_envelope\\_on\\_parsing | `False` | if set to `True` will use the envelope specified in *single_envelope* option also on parsing |\n| url_prefix | `None` | as for standard modules |\n| hostname | `None` | as for standard modules |\n\n### Customizing the database set\n\nUnder default behavior, any REST module will use `Model.all()` as the database set on every operation.\n\nWhen you need to customize it, you can use the `get_dbset` decorator. \nFor example, you may gonna use the weppy auth module:\n\n```python\nfrom myapp import auth\n\n@tasks.get_dbset\ndef fetch_tasks():\n    return auth.user.tasks\n```\n\nor you may have some soft-deletions strategies and want to expose just the records which are not deleted:\n\n```python\n@tasks.get_dbset\ndef fetch_tasks():\n    return Task.where(lambda t: t.is_deleted == False)\n```\n\n### Customizing routed methods\n\nYou can customize every route of the REST module using its `index`, `create`, `read`, `update` and `delete` decorators. In the next examples we'll override the routes with the default ones, in order to show the original code behind the default routes.\n\n```python\nfrom weppy import request\n\n@tasks.index()\ndef task_list(dbset):\n    rows = dbset.select(paginate=tasks.get_pagination())\n    return tasks.serialize_many(rows)\n```\n\nAs you can see, an *index* method should accept the `dbset` parameter, that is injected by the module. This is the default one or the one you defined with the `get_dbset` decorator.\n\n```python\n@tasks.read()\ndef task_single(row):\n    return tasks.serialize_one(row)\n```\n\nThe *read* method should accept the `row` parameter that is injected by the module. Under default behaviour the module won't call your method if it doesn't find the requested record, but instead will return a 404 HTTP response.\n\n```python\n@tasks.create()\ndef task_new():\n    attrs = tasks.parse_params()\n    resp = Task.create(**attrs)\n    if resp.errors:\n        response.status = 422\n        return tasks.error_422(resp.errors)\n    return tasks.serialize_one(resp.id)\n```\n\nThe *create* method won't need any parameters, and is responsible of creating new records in the database.\n\n```python\n@tasks.update()\ndef task_edit(dbset, rid):\n    attrs = tasks.parse_params()\n    resp = dbset.where(Task.id == rid).validate_and_update(**attrs)\n    if resp.errors:\n        response.status = 422\n        return tasks.error_422(resp.errors)\n    elif not resp.updated:\n        response.status = 404\n        return tasks.error_404()\n    return tasks.serialize_one(Task.get(rid))\n```\n\n```python\n@tasks.delete()\ndef task_del(dbset, rid):\n    deleted = dbset.where(Task.id == rid).delete()\n    if not deleted:\n        response.status = 404\n        return tasks.error_404()\n    return {}\n```\n\nThe *update* and *delete* methods are quite similar, since they should accept the `dbset` parameter and the `rid` one, which will be the record id requested by the client.\n\nAll the decorators accept an additional `pipeline` parameter that you can use to add custom pipes to the routed function:\n\n```python\n@tasks.index(pipeline=[MyCustomPipe()])\ndef task_index:\n    # code\n```\n\n### Customizing errors\n\nYou can define custom methods for the HTTP 404 and 422 errors that will generate the JSON output using the `on_404` and `on_422` decorators:\n\n```python\n@tasks.on_404\ndef task_404err():\n    return {'error': 'this is my 404 error'}\n    \n@tasks.on_422\ndef task_422err(errors):\n    return {'error': 422, 'validation': errors.as_dict()}\n```\n\n### Serialization\n\nUnder the default behaviour, the REST extension will use the `form_rw` attribute of the involved model, and overwrite the results with the contents of the `rest_rw` attribute if present.\n\nFor example, with this model:\n\n```python\nfrom weppy.orm import Model, Field\n\nclass Task(Model):\n    title = Field.string()\n    is_completed = Field.bool()\n    created_at = Field.datetime()\n    \n    form_rw = {\n        'id': False,\n        'created_at': False\n    }\n```\n\nthe REST extension will serialize just the *title* and the *is_completed* fields, while with this:\n\n```python\nfrom weppy.orm import Model, Field\n\nclass Task(Model):\n    title = Field.string()\n    is_completed = Field.bool()\n    created_at = Field.datetime()\n    \n    form_rw = {\n        'id': False,\n        'created_at': False\n    }\n    \n    rest_rw = {\n        'id': True\n    }\n```\n\nthe REST extension will serialize also the *id* field.\n\n#### Serializers\n\nWhenever you need more control over the serialization, you can use the `Serializer` class of the REST extension:\n\n```python\nfrom weppy_rest import Serializer\n\nclass TaskSerializer(Serializer):\n    attributes = ['id', 'title']\n    \ntasks = app.rest_module(\n    __name__, 'api_task', Task, serializer=TaskSerializer, url_prefix='tasks')\n```\n\nSerializers are handy when you want to add custom function to serialize something present in your rows. For instance, let's say you have a very simple tagging system:\n\n```python\nfrom weppy.orm import belongs_to, has_many\n\nclass Task(Model):\n    has_many({'tags': 'TaskTag'})\n\nclass TaskTag(Model):\n    belongs_to('task')\n    name = Field.string()\n```\n\nand you want to serialize the tags as an embedded list in your task. Then you just have to add a `tags` method to your serializer:\n\n```python\nclass TaskSerializer(Serializer):\n    attributes = ['id', 'title']\n    \n    def tags(self, row):\n        return row.tags().column('name')\n```\n\nThis is the complete list of rules that the extension will take over serializers:\n\n- `attributes` is read as first step\n- the `form_rw` and `rest_rw` attributes of the model are used to fill `attributes` list when this is empty\n- the fields in the `include` list will be added to `attributes`\n- the fields in the `exclude` list will be removed from `attributes`\n- every method defined in the serializer not starting with `_` will be called over serialization and its return value will be added to the JSON object in a key named as the method\n\nYou can also use different serialization for the list route and the other ones:\n\n```python\nfrom weppy_rest import Serializer, serialize\n\nclass TaskSerializer(Serializer):\n    attributes = ['id', 'title']\n    \nclass TaskDetailSerializer(TaskSerializer):\n    include = ['is_completed']\n    \ntasks = app.module(\n    __name__, 'api_task', Task, \n    serializer=TaskDetailSerializer, url_prefix='tasks')\n\n@tasks.index()\ndef task_list(dbset):\n    rows = dbset.select(paginate=tasks.get_pagination())\n    return serialize(rows, TaskSerializer)\n```\n\n\u003e **Note:** under default behaviour the `serialize` method will use the serializer passed to the module.\n\n### Parsing input\n\nOpposite to the serialization, you will have input parsing to parse JSON requests and perform operations on the records.\n\nUnder the default behaviour, the REST extension will use the `form_rw` attribute of the involved model, and overwrite the results with the contents of the `rest_rw` attribute if present.\n\nFor example, with this model:\n\n```python\nfrom weppy.orm import Model, Field\n\nclass Task(Model):\n    title = Field.string()\n    is_completed = Field.bool()\n    created_at = Field.datetime()\n    \n    form_rw = {\n        'id': False,\n        'created_at': False\n    }\n```\n\nthe REST extension will parse the input to allow just the *title* and the *is_completed* fields, while with this:\n\n```python\nfrom weppy.orm import Model, Field\n\nclass Task(Model):\n    title = Field.string()\n    is_completed = Field.bool()\n    created_at = Field.datetime()\n    \n    form_rw = {\n        'id': False,\n        'created_at': False\n    }\n    \n    rest_rw = {\n        'id': (True, False)\n        'created_at': True\n    }\n```\n\nthe REST extension will allow also the *created_at* field.\n\n#### Parsers\n\nVery similarly to the `Serializer` class, the extension provides also a `Parser` one:\n\n```python\nfrom weppy_rest import Parser\n\nclass TaskParser(Parser):\n    attributes = ['title']\n    \ntasks = app.rest_module(\n    __name__, app, 'api_task', Task, parser=TaskParser, url_prefix='tasks')\n```\n\nAs for serializers, you can define `attributes`, `include` and `exclude` lists in a parser, and add custom methods that will parse the params:\n\n```python\nclass TaskParser(Parser):\n    attributes = ['title']\n    \n    def created_at(self, params):\n        # some code\n```\n\nThere's also an additional attribute that you can set over a `Parser` which is the `envelope` one, if you expect to have enveloped bodies over `POST`, `PUT` and `PATCH` requests.\n\n### Pagination\n\nREST modules perform pagination over the *index* route under the default behaviour. This is performed with the `paginate` option during the select and the call to the `get_pagination` method:\n\n```python\ndef get_pagination(self):\n    try:\n        page = int(request.query_params.page or 1)\n        assert page \u003e 0\n    except:\n        page = 1\n    try:\n        page_size = int(\n            request.query_params.page_size or 20)\n        assert (10 \u003c= page_size \u003c= 25)\n    except:\n        page_size = 20\n    return page, page_size\n```\n\nYou can customize the name of the query params or the default page sizes with the extension configuration, or you can override the method completely with subclassing. \n\n### Customizing REST modules\n\n#### Extension options\n\nThis is the list of all the configuration variables available on the extension for customization – the default values are set:\n\n```python\napp.config.REST.default_module_class = RESTModule\napp.config.REST.default_serializer = Serializer\napp.config.REST.default_parser = Parser\napp.config.REST.page_param = 'page'\napp.config.REST.pagesize_param = 'page_size'\napp.config.REST.min_pagesize = 10\napp.config.REST.max_pagesize = 25\napp.config.REST.default_pagesize = 20\napp.config.REST.base_path = '/'\napp.config.REST.base_id_path = '/\u003cint:rid\u003e'\n```\n\nThis configuration will be used by all the REST modules you create, unless overridden.\n\n#### Subclassing\n\nUnder the default behavior, every REST module will use the `RESTModule` class. You can create as many subclasses from this one when you need to apply the same behaviour to several modules:\n\n```python\nfrom weppy_rest import RESTModule\n\nclass MyRESTModule(RESTModule):\n    def init(self):\n        self.disabled_methods = ['delete']\n        self.index_pipeline.append(MyCustomPipe())\n        self.list_envelope = 'objects'\n        self.single_envelope = self.model.__name__.lower()\n        \n    def _get_dbset(self):\n        return self.model.where(lambda m: m.user == session.user.id)\n        \n    def _index(self, dbset):\n        rows = dbset.select(paginate=self.get_pagination())\n        rv = self.serialize_many(rows)\n        rv['meta'] = {'total': dbset.count()}\n        return rv\n        \ntasks = app.rest_module(\n    __name__, app, 'api_task', Task, url_prefix='tasks', \n    module_class=MyRESTModule)\ntags = app.rest_module(\n    __name__, app, 'api_tag', Tag, url_prefix='tags',\n    module_class=MyRESTModule)\n```\n\nAs you can see, we defined a subclass of the `RESTModule` one and used the `init` method to customize the class initialization for our needs. We **strongly** recommend to use this method and avoid overriding the `__init__` of the class unless you really know what you're doing.\n\nUsing the `init` method, we disabled the *delete* route over the module, added a custom pipe over the *index* route and configured the envelope rules.\n\nHere is a list of variables you may want to change inside the `init` method:\n\n- model\n- serializer\n- parser\n- enabled_methods\n- disabled_methods\n- list_envelope\n- single_envelope\n- use\\_envelope\\_on\\_parsing\n\nAlso, this is the complete list of the pipeline variables and their default values:\n\n```python\ndef init(self):\n    self.index_pipeline = [SetFetcher(self)]\n    self.create_pipeline = []\n    self.read_pipeline = [SetFetcher(self), RecordFetcher(self)]\n    self.update_pipeline = [SetFetcher(self)]\n    self.delete_pipeline = [SetFetcher(self)]\n```\n\nWe've also overridden the methods for the database set retrieval and the *index* route. As you can see, these methods are starting with the `_` since are the default ones and you can still override them with decorators. This is the complete list of methods you may want to override instead of using decorators:\n\n- `_get_dbset`\n- `_index`\n- `_create`\n- `_read`\n- `_update`\n- `_delete`\n- `build_error_404`\n- `build_error_422`\n\nThere are some other methods you may need to override, like the `get_pagination` one or the serialization ones. Please, check the source code of the `RESTModule` class for further needs.\n\n## License\n\nweppy-REST is released under BSD license. Check the LICENSE file for more details.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgi0baro%2Fweppy-rest","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgi0baro%2Fweppy-rest","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgi0baro%2Fweppy-rest/lists"}