SqlAlchemy Flask-Restful Swagger Json:API OpenAPI

Overview

Latest Version Supported Python versions License: GPL v3 Python application Codacy Badge Downloads

SAFRS: Python OpenAPI & JSON:API Framework

demo

Overview

SAFRS is an acronym for SqlAlchemy Flask-Restful Swagger. The purpose of this framework is to help python developers create a self-documenting JSON API for sqlalchemy database objects and relationships. These objects can be serialized to JSON and can be created, retrieved, updated and deleted through the JSON API. Optionally, custom resource object methods can be exposed and invoked using JSON. Class and method descriptions and examples can be provided in yaml syntax in the code comments. The description is parsed and shown in the swagger web interface.

The result is an easy-to-use swagger/OpenAPI and JSON:API compliant API implementation.

A LIVE DEMO is available, where much of the basic functionality is implemented with a simple example.

UPDATE! documentation can be found in the wiki.

Installation

SAFRS can be installed as a pip package or by downloading the latest version from github, for example:

git clone https://github.com/thomaxxl/safrs
cd safrs
pip install .

Once the dependencies are installed, the examples can be started with

python3 examples/demo_relationship.py "your-interface-ip"

JSON:API Interface

Exposed resource objects can be queried using the JSON:API format. The API supports following HTTP operations:

  • GET : Retrieve an object or a list of objects
  • PATCH : Update an object.
  • DELETE: Delete an object.
  • POST : Create an object.

Please check the JSON:API spec for more implementation details. You can also try out the interface in the live demo.

Resource Objects

Database objects are implemented as subclasses of the SAFRSBase and SQLAlchemy model classes. The SQLAlchemy columns are serialized to JSON when the corresponding REST API is invoked.

Following example app illustrates how the API is built and documented:

class User(SAFRSBase, db.Model):
    """
        description: User description
    """

    __tablename__ = "Users"
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String)
    email = db.Column(db.String)

The User class is implemented as a subclass of

  • db.Model: SQLAlchemy base
  • SAFRSBase: Implements JSON serialization for the object and generates (swagger) API documentation

This User object is then exposed through the web interface using the Api object

api.expose_object(User)

The User object REST methods are available on /User, the swagger schema is available on /api/swagger.json and the UI is available on /api/: User Swagger

Relationships

Database object such as the User class from the demo.py example can be extended to include relationships with other objects. The demo_relationship.py contains following extension of the User class where a relationship with the Book class is implemented:

class User(SAFRSBase, db.Model):
    '''
        description: User description
    '''
    __tablename__ = 'Users'
    id = db.Column(db.String, primary_key=True)
    name = db.Column(db.String, default='')
    email = db.Column(db.String, default='')
    books = db.relationship('Book', back_populates="user")
...

A many-to-one database association is declared by the back_populates relationship argument. The Book class is simply another subclass of SAFRSBase and db.Model, similar to the previous User class:

class Book(SAFRSBase, db.Model):
    '''
        description: Book description
    '''
    __tablename__ = 'Books'
    id = db.Column(db.String, primary_key=True)
    name = db.Column(db.String, default='')
    user_id = db.Column(db.String, db.ForeignKey('Users.id'))
    user = db.relationship('User', back_populates='books')

The User.book relationship can be queried in the API through the following endpoints: Relations Swagger

  • POST adds an item to the relationship
  • DELETE removes an item from the relationship
  • GET retrieves a list of item ids

The relationship API endpoints work similarly for one-to-many relationships.

Relationship members can also be included in the response when querying an instance, by specifying the relationship names as a comma separated list in the include query argument.

relationship include swagger

For example, to retrieve all items in the books_read relationship from the People endpoint, you may add the include=books_read url parameter

http://thomaxxl.pythonanywhere.com/api/People/?include=books_read

To retrieve nested relationship items, you can specify the nested relationship name after the '.', to retrieve the authors of the books_read instances for instance, you can use

http://thomaxxl.pythonanywhere.com/api/People/?include=books_read.author

Methods

Custom Methods

Safrs allows the user to implement custom methods on the exposed objects. This methods can be invoked through the json API by sending an HTTP POST request to the method endpoint The following example implements a "send_mail" method fro example:

class User(SAFRSBase, db.Model):
    '''
        description: User description
    '''
    __tablename__ = 'Users'
    id = Column(String, primary_key=True)
    name = Column(String, default='')
    email = Column(String, default='')

    # Following method is exposed through the REST API 
    # This means it can be invoked with a HTTP POST
    @jsonapi_rpc(http_methods=['POST','GET'])
    def send_mail(self, email):
        '''
            description : Send an email
            args:
                email:
                    type : string 
                    example : test email
        '''
        content = 'Mail to {} : {}\n'.format(self.name, email)
        return { 'result' : 'sent {}'.format(content)}

This method shows up in the swagger interface:

Method Swagger

The send_mail method is documented with the jsonapi_rpc decorator. This decorator generates a schema based on the function documentation. This documentation contains yaml specification of the API which is used by the swagger UI.

api_methods.py contains a couple of methods that can be used in your models.

The yaml specification has to be in the first part of the function and class comments. These parts are delimited by four dashes ("----") . The rest of the comment may contain additional documentation.

Class Methods

Two class-level methods have been defined to facilitate object retrieval:

  • lookup : retrieve a list of objects that match the argument list. For example, following HTTP POST request to a container will retrieve a list of itemswhere the name is "thomas"
{
  "method": "lookup",
  "args": {
    "name": "thomas"
  }
}
  • get_list : retrieve a list of the items with the specified ID's

Application Initialization

The API can be initialized like this:

api = SAFRSAPI(app, host=HOST, port=PORT, prefix=API_PREFIX)

Then you can expose objects with

api.expose_object(User)    

An example that uses the flask app factory pattern is implement in examples/mini_app.py

app.config.update(DEBUG=True)

Endpoint Naming

As can be seen in the swagger UI:

  • the endpoint collection path names are the SQLAlchemy __tablename__ properties (e.g. /Users )
  • the parameter names are derived from the SAFRSBase class names (e.g. {UserId} )
  • the the relationship names are the SAFRSBase class relationship names (e.g /books ) The URL path format is configurable

Configuration

Some configuration parameters can be set in config.py:

  • USE_API_METHODS: set this to false in case you want to disable the jsonapi_rpc functionality
  • INSTANCE_URL_FMT: This parameter declares the instance url path format
  • RELATIONSHIP_URL_FMT: This parameter declares the relationship endpoint path format

Exposing Existing Databases

Safrs allows you to Expose existing databases as jsona:api services with the expose_existing.py script, for example:

python3 expose_existing.py mysql+pymysql://root:[email protected]/sakila --host localhost

This script uses sqlacodegen to generate a source file containing the SQLAlchemy and SAFRSBase database models and starts the API webservice.

More details here

More Examples and Use Cases

The examples folder contains more example scripts:

  • Using a sha hash as primary key (id)
  • CORS usage
  • Flask-Admin integration example, eg.: demo

A docker image can be found here: https://github.com/thomaxxl/safrs-example

Advanced Usage

Filtering

The swagger shows the jsonapi filters that can be used in the url query arguments. Items with an exact match of the specified attribute value can be fetched by specifying the corresponding key-value query parameter. For example, suppose the User class, exposed at /Users has a name attribute, to retrieve all instances with the name "John", you can use a GET request to /Users?filter[name]=John.

It is also possible to use more generic filters by specifiying a JSON string, for example filter=[{"name":"timestamp","op":"gt","val":"2020-08-01"},{"name":"timestamp","op":"lt","val":"2020-08-02"}].

More info can be found in the wiki.

Custom Serialization

Serialization and deserialization are implemented by the SAFRSBase to_dict and __init__ : you can extend these methods as usual. For example, if you would like to add some attributes to the json payload of the User object, you can override the to_dict method:

class User(SAFRSBase, db.Model):
    '''
        description: User description
    '''
    __tablename__ = 'Users'
    id = db.Column(db.String, primary_key=True)
    name = db.Column(db.String, default='')
    email = db.Column(db.String, default='')
    books = db.relationship('Book', back_populates="user")

    def to_dict(self):
        result = SAFRSBase.to_dict(self)
        result['custom_field'] = 'custom'
        return result

This will add the custom_field attribute to the result attributes:

"attributes": {
    "custom_field": "custom",
    "email": "reader_email0",
    "name": "Reader 0"
}

Excluding Attributes and Relationships

It is possible to specify attributes and relationships that should not be serialized by specifying the respective exclude_attrs and èxclude_rels` class attributes in your SAFRSBase instances. Examples can be found here and here

Limiting HTTP Methods

It is possible to limit the HTTP methods that are allowed by overriding the http_methods class attribute. An example can be found here

HTTP Decorators

The decorators class attribute list can be used to add custom decorators to the HTTP endpoints. An example of this functionality is implemented in the authentication examples.

API Methods

Some additional API RPC methods are implemented in api_methods.py, e.g. mysql regex search.

Custom swagger

The swagger schema can be merged with a modified schema dictionary by supplying the to-be-merged dictionary as the custom_swagger argument to SAFRSAPI, e.g.

custom_swagger = {"info": {"title" : "New Title" }} # Customized swagger title will be merged
api = SAFRSAPI(app, host=swagger_host, port=PORT, prefix=OAS_PREFIX, api_spec_url=OAS_PREFIX+'/swagger',
               custom_swagger=custom_swagger, schemes=['http', 'https'], description=description)

Classes Without SQLAlchemy Models

You can implement a serializable class without a model but this requires some extra work because safrs needs to know which attributes and relationships to serialize. An example is implemented here

More Customization

The documentation is being moved to the wiki

Limitations & TODOs

This code was developed for a specific use-case and may not be flexible enough for everyone's needs. A lot of the functionality is available but not documented for the sake of brevity. Performance is reasonable for regular databases, but once you start exposing really big tables you may run into problems, for example: the count() for mysql innodb is slow on large(1M rows) tables, a workaround can be implemented by querying the sys tables or using werkzeug caching. Feel free to open an issue or drop me an email if you run into problems or something isn't clear!

References

Thanks

I developed this code when I worked at Excellium Services. They allowed me to open source it when I stopped working there.

Comments
  • Run in example demo_relationship.py creating users or books in flask_admin returns Integrity Errors

    Run in example demo_relationship.py creating users or books in flask_admin returns Integrity Errors

    Integrity error. (sqlite3.IntegrityError) NOT NULL constraint failed: Users.id [SQL: INSERT INTO "Users" (name, email) VALUES (?, ?)] [parameters: ('user4711', '[email protected]')] (Background on this error at: http://sqlalche.me/e/13/gkpj)

    Primary Key is missing in the Insert Syntax.

    class User(SAFRSBase, db.Model): tablename = "Users" id = db.Column(db.String, primary_key=True)

    opened by agoe 17
  •  Object of type 'time' is not JSON serializable

    Object of type 'time' is not JSON serializable

    Hi, very nice app. My database makes use of the "Time" column. I get this error: Object of type 'time' is not JSON serializable. I tried to add a custom serialization like below without success. How should I handle this?

    in base.py I see this comment """ Parse datetime and date values for some common representations If another format is uses, the user should create a custom column type or custom serialization """

    def to_dict(self):
        return {c.key: getattrOnType(self, c) for c in inspect(self).mapper.column_attrs}
    

    def getattrOnType(self, c): if type(getattr(self, c.key)) is datetime.datetime or
    type(getattr(self, c.key)) is datetime.time or
    type(getattr(self, c.key)) is datetime.date or
    type(getattr(self, c.key)) is decimal.Decimal: return str(getattr(self, c.key)) elif (getattr(self, c.key)): return getattr(self, c.key) else: # allows you to handle null values differently return getattr(self, c.key)

    opened by NexPlex 16
  • jsonapi_rpc response structure

    jsonapi_rpc response structure

    Hello again! Here's another request regarding jsonapi_rpc :)

    So I've got a working endpoint now - beautifully documented - generating an object with relations and all. Great stuff! The next step is of course returning this object to the client in a jsonapi compliant fashion. I was hoping to simply do return obj.to_dict(), but that gives me this response:

    {
      "meta": {
        "result": {
          "name": "test",
          "description": "text etc"
        }
      }
    }
    

    I realise this structure probably originates from an idea of jsonapi_rpc-methods performing arbitrary tasks and returning some information related to that task. But in some cases (like mine :)) these endpoints could easily want to return an instance of the model they belong to.

    What do you think? Would be possible to give more control over the type of response to the method declaration somehow? Or just leave it up to the method completely to form its response? If the latter is preferred - what relevant internals should I be looking at for building a jsonapi compliant resource object etc?

    opened by wicol 14
  • Custom documented classmethod to execute advanced GET

    Custom documented classmethod to execute advanced GET

    Hello,

    we created a custom documented (class)method to do advanced search on a table. Our code:

    @classmethod
    @documented_api_method
    def search(cls, pattern, page_offset, page_limit, sort):
        """
        description : Search a client by pattern in most important fields
        args:
            pattern:
                en
            page_offset:
                0
            page_limit:
                10
            sort:
                first_name,last_name
        """
        request.args = dict(request.args)
        request.args['page[offset]'] = page_offset
        request.args['page[limit]'] = page_limit
        meta = {}
        errors = None
        jsonapi = dict(version='1.0')
        limit = request.args.get('limit', UNLIMITED)
        meta['limit'] = limit
        # retrieve a collection
        pattern = f'%{pattern}%'
        instances = cls.query.filter(or_(
            Client.first_name.like(pattern),
            Client.middle_name.like(pattern),
            Client.last_name.like(pattern),
        ))
        instances = jsonapi_sort(instances, cls)
        links, instances = paginate(instances)
        data = [item for item in instances]
        included = get_included(data, limit)
        result = dict(data=data)
        if errors:
            result['errors'] = errors
        if meta:
            result['meta'] = meta
        if jsonapi:
            result['jsonapi'] = jsonapi
        if links:
            result['links'] = links
        if included:
            result['included'] = included
        return result
    

    The result is a generated POST endpoint, but we would like to see a GET, since we are getting data. The search items could be passed as URL arguments instead of in the body. Also, because we have a POST, we can't show an example value in the swagger UI.

    Example curl:

    curl -X POST --header 'Content-Type: application/json' --header 'Accept: application/json' -d '{ \ 
       "meta": { \ 
         "method": "search", \ 
         "args": { \ 
           "pattern": "en", \ 
           "page_offset": 1, \ 
           "page_limit": 1, \ 
           "sort": "first_name,last_name" \ 
         } \ 
       } \ 
     }' 'http://localhost:5000/client/search'
    

    Linked issue: https://github.com/thomaxxl/safrs/issues/6

    Can you provide a better method? Are we doing it right?

    opened by DennyWeinberg 12
  • error on json encode for column with func as default

    error on json encode for column with func as default

    I made a pretty simple mixin as seen below but the code seems to die on the dt.uctnow

    from datetime import datetime as dt
    
    
    class CreatedMixin:
        created_at = db.Column(db.DateTime(timezone=True), default=dt.utcnow)
        updated_at = db.Column(db.DateTime(timezone=True), default=dt.utcnow, onupdate=dt.utcnow)
    
        @classmethod
        def newest(cls, amount=5):
            assert isinstance(cls, Model)
            return cls.query.order_by(cls.created_at.desc()).limit(amount)
    
        @classmethod
        def updated(cls, amount=5):
            assert isinstance(cls, Model)
            return cls.query.order_by(cls.updated_at.desc()).limit(amount)
    

    image

    stacktrace

    127.0.0.1 - - [18/Mar/2019 14:00:46] "GET /swagger.json HTTP/1.1" 500 -
    Traceback (most recent call last):
      File "/home/patrick/PycharmProjects/ebookhub-backend/venv/lib/python3.6/site-packages/flask/app.py", line 2309, in __call__
        return self.wsgi_app(environ, start_response)
      File "/home/patrick/PycharmProjects/ebookhub-backend/venv/lib/python3.6/site-packages/flask/app.py", line 2295, in wsgi_app
        response = self.handle_exception(e)
      File "/home/patrick/PycharmProjects/ebookhub-backend/venv/lib/python3.6/site-packages/flask_restful/__init__.py", line 273, in error_router
        return original_handler(e)
      File "/home/patrick/PycharmProjects/ebookhub-backend/venv/lib/python3.6/site-packages/flask_restful/__init__.py", line 273, in error_router
        return original_handler(e)
      File "/home/patrick/PycharmProjects/ebookhub-backend/venv/lib/python3.6/site-packages/flask_cors/extension.py", line 161, in wrapped_function
        return cors_after_request(app.make_response(f(*args, **kwargs)))
      File "/home/patrick/PycharmProjects/ebookhub-backend/venv/lib/python3.6/site-packages/flask/app.py", line 1741, in handle_exception
        reraise(exc_type, exc_value, tb)
      File "/home/patrick/PycharmProjects/ebookhub-backend/venv/lib/python3.6/site-packages/flask/_compat.py", line 34, in reraise
        raise value.with_traceback(tb)
      File "/home/patrick/PycharmProjects/ebookhub-backend/venv/lib/python3.6/site-packages/flask/app.py", line 2292, in wsgi_app
        response = self.full_dispatch_request()
      File "/home/patrick/PycharmProjects/ebookhub-backend/venv/lib/python3.6/site-packages/flask/app.py", line 1815, in full_dispatch_request
        rv = self.handle_user_exception(e)
      File "/home/patrick/PycharmProjects/ebookhub-backend/venv/lib/python3.6/site-packages/flask_restful/__init__.py", line 273, in error_router
        return original_handler(e)
      File "/home/patrick/PycharmProjects/ebookhub-backend/venv/lib/python3.6/site-packages/flask_restful/__init__.py", line 273, in error_router
        return original_handler(e)
      File "/home/patrick/PycharmProjects/ebookhub-backend/venv/lib/python3.6/site-packages/flask_cors/extension.py", line 161, in wrapped_function
        return cors_after_request(app.make_response(f(*args, **kwargs)))
      File "/home/patrick/PycharmProjects/ebookhub-backend/venv/lib/python3.6/site-packages/flask/app.py", line 1718, in handle_user_exception
        reraise(exc_type, exc_value, tb)
      File "/home/patrick/PycharmProjects/ebookhub-backend/venv/lib/python3.6/site-packages/flask/_compat.py", line 34, in reraise
        raise value.with_traceback(tb)
      File "/home/patrick/PycharmProjects/ebookhub-backend/venv/lib/python3.6/site-packages/flask/app.py", line 1813, in full_dispatch_request
        rv = self.dispatch_request()
      File "/home/patrick/PycharmProjects/ebookhub-backend/venv/lib/python3.6/site-packages/flask_debugtoolbar/__init__.py", line 125, in dispatch_request
        return view_func(**req.view_args)
      File "/usr/lib/python3.6/cProfile.py", line 109, in runcall
        return func(*args, **kw)
      File "/home/patrick/PycharmProjects/ebookhub-backend/venv/lib/python3.6/site-packages/flask_restful/__init__.py", line 484, in wrapper
        return self.make_response(data, code, headers=headers)
      File "/home/patrick/PycharmProjects/ebookhub-backend/venv/lib/python3.6/site-packages/flask_restful/__init__.py", line 513, in make_response
        resp = self.representations[mediatype](data, *args, **kwargs)
      File "/home/patrick/PycharmProjects/ebookhub-backend/venv/lib/python3.6/site-packages/flask_restful/representations/json.py", line 21, in output_json
        dumped = dumps(data, **settings) + "\n"
      File "/usr/lib/python3.6/json/__init__.py", line 238, in dumps
        **kw).encode(obj)
      File "/usr/lib/python3.6/json/encoder.py", line 201, in encode
        chunks = list(chunks)
      File "/usr/lib/python3.6/json/encoder.py", line 430, in _iterencode
        yield from _iterencode_dict(o, _current_indent_level)
      File "/usr/lib/python3.6/json/encoder.py", line 404, in _iterencode_dict
        yield from chunks
      File "/usr/lib/python3.6/json/encoder.py", line 404, in _iterencode_dict
        yield from chunks
      File "/usr/lib/python3.6/json/encoder.py", line 404, in _iterencode_dict
        yield from chunks
      File "/usr/lib/python3.6/json/encoder.py", line 404, in _iterencode_dict
        yield from chunks
      File "/usr/lib/python3.6/json/encoder.py", line 404, in _iterencode_dict
        yield from chunks
      File "/usr/lib/python3.6/json/encoder.py", line 404, in _iterencode_dict
        yield from chunks
      File "/usr/lib/python3.6/json/encoder.py", line 404, in _iterencode_dict
        yield from chunks
      File "/usr/lib/python3.6/json/encoder.py", line 437, in _iterencode
        o = _default(o)
      File "/usr/lib/python3.6/json/encoder.py", line 180, in default
        o.__class__.__name__)
    TypeError: Object of type 'function' is not JSON serializable
    
    opened by patvdleer 10
  • jsonapi_rpc parameters and swagger

    jsonapi_rpc parameters and swagger

    I'm trying to expose a custom endpoint related to a model. It's an endpoint for generating a new object given a value, so I created something like this:

    @classmethod
    @jsonapi_rpc(http_methods=["GET"])
    def get_by_name(cls, name):
        """
            description : Generate and return a Thing based on name
            args:
                name:
                    type : string
                    example : thingy
            pageable: false
        """
        thing = cls(name=name)
        thing.description = populate_based_on_name()
        db.session.add(thing)
        db.session.commit()
        return thing.to_dict()
    

    An endpoint is created and it does appear in the swagger ui, but the swagger docs are rather confusing; containing references to "page[offset]", "include", "sort", "filter" etc. It doesn't seem to be picking up on my docstring here.

    It also seems like only one parameter, called "varargs", is supported?

    Is there any way I can better control the docs generated and get a properly named parameter working? I could probably get parameters from the request instead of method args, but I'd still need to get the docs under control.

    opened by wicol 9
  • Add query operators to json api

    Add query operators to json api

    Hi Tomaxx,

    I don't know if this can be done already as I didn't go through the code thoroughly. I want to be able to query stuff based on ranges. For instance if I have a timestamp field and I want to query a range with this patch you can do a GET for instance to http://localhost:5000/?filter[timestamp]=>2020-08-01and<2020-08-03 to retrieve entries between those 2 dates.

    This patch allows 2 types of queries:

    • op value Where op can be <,<=,>,>=
    • op1 value bop op2 value Where op1 and op2 are like op. And bop is one of "and", "or".

    Please share your opinion.

    opened by SergioSoldado 8
  • Returing N rows with no relations generates N superfluous selects

    Returing N rows with no relations generates N superfluous selects

    Hi,

    I stumbled upon this issue recently while returning larger datasets.

    When doing a trivial select on tables with no relations, after the correctly formatted select returning N rows, N more selects with seemingly no cause are executed.

    The trivial example below demonstrates this

    Executing this query returning 10 rows /items/?fields%5BItem%5D=foo%2Cbar&page%5Boffset%5D=0&filter%5Bbar%5D=group_0

    results in 10 instances of:

    SELECT items.id AS items_id, items.foo AS items_foo, items.bar AS items_bar
    FROM items
    WHERE items.id = ?
     LIMIT ? OFFSET ?
    INFO sqlalchemy.engine.base.Engine ('', 1, 0)
    

    Code for reproduction:

    import sys
    from flask import Flask
    from flask_sqlalchemy import SQLAlchemy
    from safrs import SAFRSBase, SAFRSAPI
    from flask_sqlalchemy import SQLAlchemy
    import sqlite3
    import logging
    
    db = SQLAlchemy()
    logging.basicConfig(level=logging.DEBUG)
    
    class Item(SAFRSBase, db.Model):
        __tablename__ = "items"
        id = db.Column(db.String(32), primary_key=True)
        foo = db.Column(db.String(32))
        bar = db.Column(db.String(32))
    
    def start_app(app):
    
        api = SAFRSAPI(app, host=HOST)
        api.expose_object(Item)
        
        for i in range(20):
            item =Item(foo="item_" + str(i), bar="group_" + str((int)(i / 10)))
        
        print("Starting API: http://{}:{}/api".format(HOST, PORT))
        app.run(host=HOST, port=PORT)
    
    app = Flask("demo_app")
    app.config.update(
        SQLALCHEMY_DATABASE_URI="sqlite://",
        SQLALCHEMY_TRACK_MODIFICATIONS=False,
        SECRET_KEY=b"sdqfjqsdfqizroqnxwc",
        DEBUG=True,
        SQLALCHEMY_ECHO = True
    )
    
    HOST = sys.argv[1] if len(sys.argv) > 1 else "0.0.0.0"
    PORT = 5000
    db.init_app(app)
    
    with app.app_context():
        db.create_all()
        start_app(app)
    
    opened by tomaswangen 8
  • Related instaces on init

    Related instaces on init

    I'm trying out SAFRS by re-implementing an existing API based on flask-sqlalchemy. One issue I'm facing trying to re-use the migration files is passing in related instances on model init:

    # Doesn't work but can be worked around using parent_id=some_obj.id
    obj = MyModel(parent=some_obj)
    # Doesn't work and the workaround is a bit messy
    obj = MyModel(children=[c1, c2])
    # In my migration I'm actually doing this which works natively with sqlalchemy
    obj = MyModel(children=[Child(), Child()])
    

    This continue looks to be the culprit?: https://github.com/thomaxxl/safrs/blob/master/safrs/db.py#L144

    What's the story there? Would it be possible to just un-comment that stuff (or what about calling super to let sqlalchemy deal with this natively..?)

    Edit: FYI my workaround for now is using this ABC:

    class BaseModel(SAFRSBase, db.Model):
        __abstract__ = True
        db_commit = False
    
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            db.Model.__init__(self, *args, **kwargs)
    
    opened by wicol 8
  • Invalid swagger for

    Invalid swagger for "properties" under "definitions" breaks defintions display and validation

    The value for "properties" under "definitions" comes in as a string and not as a JSON object when the swagger.json is generated. This blocks definitions from showing up in the swagger UI and throws validity errors when trying to edit via the Swagger Editor.

    The swagger editor complains that "properties must be an object".

    Example snippet:

    "definitions": {
            "jsonapi_error_403": {
                "properties": "{'errors': {'example': [{'title': 'Request forbidden -- authorization will not help', 'detail': '', 'code': '403'}], 'type': 'string'}}",
                "description": ""
            },
            "jsonapi_error_404": {
                "properties": "{'errors': {'example': [{'title': 'Nothing matches the given URI', 'detail': '', 'code': '404'}], 'type': 'string'}}",
                "description": ""
            }
    }
    
    opened by gwengullett 7
  • allow_client_generated_ids ignored in POST

    allow_client_generated_ids ignored in POST

    Thank you for a great library that is saving me tons of time!

    I have a problem using allow_client_generated_ids on one of my model-classes: Unless I add a line in safrs/base.py:_s_post() I get an exception claiming that my "client generated id" (the one I needed to set myself) is None. - I think it was incorrectly removed without checking the truth of cls.allow_client_generated_ids.

    My solution is to modify the beginning of _s_post():

            if getattr(cls, "allow_client_generated_ids", False) is False:
                for col_name in cls.id_type.column_names:
                    ...
    

    (After this change I still needed to make sure that the class hat a GET as well as a POST-method (since it could be accessed via a relationship I thought I could get away without the GET), but then everything worked like a charm! :-)

    opened by frip 6
  • Bump fast-json-patch from 3.1.0 to 3.1.1 in /swagger-editor

    Bump fast-json-patch from 3.1.0 to 3.1.1 in /swagger-editor

    Bumps fast-json-patch from 3.1.0 to 3.1.1.

    Release notes

    Sourced from fast-json-patch's releases.

    3.1.1

    Security Fix for Prototype Pollution - huntr.dev #262

    Commits

    Dependabot compatibility score

    Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


    Dependabot commands and options

    You can trigger Dependabot actions by commenting on this PR:

    • @dependabot rebase will rebase this PR
    • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
    • @dependabot merge will merge this PR after your CI passes on it
    • @dependabot squash and merge will squash and merge this PR after your CI passes on it
    • @dependabot cancel merge will cancel a previously requested merge and block automerging
    • @dependabot reopen will reopen this PR if it is closed
    • @dependabot close will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
    • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
    • @dependabot use these labels will set the current labels as the default for future PRs for this repo and language
    • @dependabot use these reviewers will set the current reviewers as the default for future PRs for this repo and language
    • @dependabot use these assignees will set the current assignees as the default for future PRs for this repo and language
    • @dependabot use this milestone will set the current milestone as the default for future PRs for this repo and language

    You can disable automated security fix PRs for this repo from the Security Alerts page.

    dependencies javascript 
    opened by dependabot[bot] 0
  • Bump qs from 6.5.2 to 6.5.3 in /swagger-editor

    Bump qs from 6.5.2 to 6.5.3 in /swagger-editor

    Bumps qs from 6.5.2 to 6.5.3.

    Changelog

    Sourced from qs's changelog.

    6.5.3

    • [Fix] parse: ignore __proto__ keys (#428)
    • [Fix] utils.merge: avoid a crash with a null target and a truthy non-array source
    • [Fix] correctly parse nested arrays
    • [Fix] stringify: fix a crash with strictNullHandling and a custom filter/serializeDate (#279)
    • [Fix] utils: merge: fix crash when source is a truthy primitive & no options are provided
    • [Fix] when parseArrays is false, properly handle keys ending in []
    • [Fix] fix for an impossible situation: when the formatter is called with a non-string value
    • [Fix] utils.merge: avoid a crash with a null target and an array source
    • [Refactor] utils: reduce observable [[Get]]s
    • [Refactor] use cached Array.isArray
    • [Refactor] stringify: Avoid arr = arr.concat(...), push to the existing instance (#269)
    • [Refactor] parse: only need to reassign the var once
    • [Robustness] stringify: avoid relying on a global undefined (#427)
    • [readme] remove travis badge; add github actions/codecov badges; update URLs
    • [Docs] Clean up license text so it’s properly detected as BSD-3-Clause
    • [Docs] Clarify the need for "arrayLimit" option
    • [meta] fix README.md (#399)
    • [meta] add FUNDING.yml
    • [actions] backport actions from main
    • [Tests] always use String(x) over x.toString()
    • [Tests] remove nonexistent tape option
    • [Dev Deps] backport from main
    Commits
    • 298bfa5 v6.5.3
    • ed0f5dc [Fix] parse: ignore __proto__ keys (#428)
    • 691e739 [Robustness] stringify: avoid relying on a global undefined (#427)
    • 1072d57 [readme] remove travis badge; add github actions/codecov badges; update URLs
    • 12ac1c4 [meta] fix README.md (#399)
    • 0338716 [actions] backport actions from main
    • 5639c20 Clean up license text so it’s properly detected as BSD-3-Clause
    • 51b8a0b add FUNDING.yml
    • 45f6759 [Fix] fix for an impossible situation: when the formatter is called with a no...
    • f814a7f [Dev Deps] backport from main
    • Additional commits viewable in compare view

    Dependabot compatibility score

    Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


    Dependabot commands and options

    You can trigger Dependabot actions by commenting on this PR:

    • @dependabot rebase will rebase this PR
    • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
    • @dependabot merge will merge this PR after your CI passes on it
    • @dependabot squash and merge will squash and merge this PR after your CI passes on it
    • @dependabot cancel merge will cancel a previously requested merge and block automerging
    • @dependabot reopen will reopen this PR if it is closed
    • @dependabot close will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
    • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
    • @dependabot use these labels will set the current labels as the default for future PRs for this repo and language
    • @dependabot use these reviewers will set the current reviewers as the default for future PRs for this repo and language
    • @dependabot use these assignees will set the current assignees as the default for future PRs for this repo and language
    • @dependabot use this milestone will set the current milestone as the default for future PRs for this repo and language

    You can disable automated security fix PRs for this repo from the Security Alerts page.

    dependencies javascript 
    opened by dependabot[bot] 0
  • Bump decode-uri-component from 0.2.0 to 0.2.2 in /swagger-editor

    Bump decode-uri-component from 0.2.0 to 0.2.2 in /swagger-editor

    Bumps decode-uri-component from 0.2.0 to 0.2.2.

    Release notes

    Sourced from decode-uri-component's releases.

    v0.2.2

    • Prevent overwriting previously decoded tokens 980e0bf

    https://github.com/SamVerschueren/decode-uri-component/compare/v0.2.1...v0.2.2

    v0.2.1

    • Switch to GitHub workflows 76abc93
    • Fix issue where decode throws - fixes #6 746ca5d
    • Update license (#1) 486d7e2
    • Tidelift tasks a650457
    • Meta tweaks 66e1c28

    https://github.com/SamVerschueren/decode-uri-component/compare/v0.2.0...v0.2.1

    Commits

    Dependabot compatibility score

    Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


    Dependabot commands and options

    You can trigger Dependabot actions by commenting on this PR:

    • @dependabot rebase will rebase this PR
    • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
    • @dependabot merge will merge this PR after your CI passes on it
    • @dependabot squash and merge will squash and merge this PR after your CI passes on it
    • @dependabot cancel merge will cancel a previously requested merge and block automerging
    • @dependabot reopen will reopen this PR if it is closed
    • @dependabot close will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
    • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
    • @dependabot use these labels will set the current labels as the default for future PRs for this repo and language
    • @dependabot use these reviewers will set the current reviewers as the default for future PRs for this repo and language
    • @dependabot use these assignees will set the current assignees as the default for future PRs for this repo and language
    • @dependabot use this milestone will set the current milestone as the default for future PRs for this repo and language

    You can disable automated security fix PRs for this repo from the Security Alerts page.

    dependencies javascript 
    opened by dependabot[bot] 0
  • Bump loader-utils from 1.2.3 to 1.4.2 in /swagger-editor

    Bump loader-utils from 1.2.3 to 1.4.2 in /swagger-editor

    Bumps loader-utils from 1.2.3 to 1.4.2.

    Release notes

    Sourced from loader-utils's releases.

    v1.4.2

    1.4.2 (2022-11-11)

    Bug Fixes

    v1.4.1

    1.4.1 (2022-11-07)

    Bug Fixes

    v1.4.0

    1.4.0 (2020-02-19)

    Features

    • the resourceQuery is passed to the interpolateName method (#163) (cd0e428)

    v1.3.0

    1.3.0 (2020-02-19)

    Features

    • support the [query] template for the interpolatedName method (#162) (469eeba)
    Changelog

    Sourced from loader-utils's changelog.

    1.4.2 (2022-11-11)

    Bug Fixes

    1.4.1 (2022-11-07)

    Bug Fixes

    1.4.0 (2020-02-19)

    Features

    • the resourceQuery is passed to the interpolateName method (#163) (cd0e428)

    1.3.0 (2020-02-19)

    Features

    • support the [query] template for the interpolatedName method (#162) (469eeba)

    Commits

    Dependabot compatibility score

    Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


    Dependabot commands and options

    You can trigger Dependabot actions by commenting on this PR:

    • @dependabot rebase will rebase this PR
    • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
    • @dependabot merge will merge this PR after your CI passes on it
    • @dependabot squash and merge will squash and merge this PR after your CI passes on it
    • @dependabot cancel merge will cancel a previously requested merge and block automerging
    • @dependabot reopen will reopen this PR if it is closed
    • @dependabot close will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
    • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
    • @dependabot use these labels will set the current labels as the default for future PRs for this repo and language
    • @dependabot use these reviewers will set the current reviewers as the default for future PRs for this repo and language
    • @dependabot use these assignees will set the current assignees as the default for future PRs for this repo and language
    • @dependabot use this milestone will set the current milestone as the default for future PRs for this repo and language

    You can disable automated security fix PRs for this repo from the Security Alerts page.

    dependencies javascript 
    opened by dependabot[bot] 0
  • Read-only Columns

    Read-only Columns

    I was wondering if it's possible to create columns which are read-only when exposed via the API.

    For example, I have a data model with a last_completed column which I want to restrict to being set or updated only via a call to a custom RPC call named complete, exposed with @jsonapi_rpc. It also has some computed values such as due_date which are exposed with @jsonapi_attr:

    import datetime
    from dateutil.relativedelta import relativedelta
    from safrs import SAFRSBase, jsonapi_attr, jsonapi_rpc
    from typing import Optional
    
    from app import db
    
    
    class Task(SAFRSBase, db.Model):
        __tablename__ = 'tasks'
        id = db.Column(db.Integer, primary_key=True)
        last_completed = db.Column(db.Date, nullable=True)
        name = db.Column(db.String)
    
        @jsonapi_rpc(http_methods=["POST"])
        def complete(self):
            self.last_completed = datetime.datetime.now().date()
    
        @jsonapi_attr
        def due_date(self) -> Optional[datetime.datetime]:
            if self.last_completed:
                return self.last_completed + relativedelta(
                    **{self.frequency_unit.value: self.frequency_value}
                )
            else:
                return None
    

    To clarify, I would like last_completed and due_date to be available via GET but not PATCH or POST.

    Attempting to set such a restriction with last_completed.http_methods = ['GET'] as suggested for relationships here doesn't work.

    Perhaps there is something similar to the hidden columns feature that I am not seeing?

    Many thanks :)

    opened by JM0804 2
  • `definitions` property in swagger.json contains only stubs

    `definitions` property in swagger.json contains only stubs

    Hello,

    I noticed that the definitions property in the generated swagger.json seems to contain only stub objects, with a single data property of type string. This is problematic, because clients generated with swagger-codegen read the definitions property to generate model code on the client. This is particularly important for typed languages. It would be better if the definitions object were to contain complete JSON schemas for each endpoint.

    Is this in-scope for the project? if I were to contribute it, would you accept a patch?

    Thank you for looking into this.

    opened by jbeard4 8
Releases(2.12.5)
Owner
Thomas Pollet
Thomas Pollet
Some code that takes a pipe-separated input and converts that into a table!

tablemaker A program that takes an input: a | b | c # With comments as well. e | f | g h | i |jk And converts it to a table: ┌───┬───┬────┐ │ a │ b │

CodingSoda 2 Aug 30, 2022
Xanadu Quantum Codebook is an experimental, exercise-based introduction to quantum computing using PennyLane.

Xanadu Quantum Codebook The Xanadu Quantum Codebook is an experimental, exercise-based introduction to quantum computing using PennyLane. This reposit

Xanadu 43 Dec 09, 2022
Fastest Git client for Emacs.

EAF Git Client EAF Git is git client application for the Emacs Application Framework. The advantages of EAF Git are: Large log browse: support 1 milli

Emacs Application Framework 31 Dec 02, 2022
🍭 epub generator for lightnovel.us 轻之国度 epub 生成器

lightnovel_epub 本工具用于基于轻之国度网页生成epub小说。 注意:本工具仅作学习交流使用,作者不对内容和使用情况付任何责任! 原理 直接抓取 HTML,然后将其中的图片下载至本地,随后打包成 EPUB。

gyro永不抽风 188 Dec 30, 2022
PowerApps-docstring is a console based, pipeline ready application that automatically generates user and technical documentation for Power Apps.

powerapps-docstring PowerApps-docstring is a console based, pipeline ready application that automatically generates user and technical documentation f

Sebastian Muthwill 30 Nov 23, 2022
Fast syllable estimation library based on pattern matching.

Syllables: A fast syllable estimator for Python Syllables is a fast, simple syllable estimator for Python. It's intended for use in places where speed

ProseGrinder 26 Dec 14, 2022
30 days of Python programming challenge is a step-by-step guide to learn the Python programming language in 30 days

30 days of Python programming challenge is a step-by-step guide to learn the Python programming language in 30 days. This challenge may take more than100 days, follow your own pace.

Asabeneh 17.7k Jan 07, 2023
This repo provides a package to automatically select a random seed based on ancient Chinese Xuanxue

🤞 Random Luck Deep learning is acturally the alchemy. This repo provides a package to automatically select a random seed based on ancient Chinese Xua

Tong Zhu(朱桐) 33 Jan 03, 2023
freeCodeCamp Scientific Computing with Python Project for Certification.

Polygon_Area_Calculator freeCodeCamp Python Project freeCodeCamp Scientific Computing with Python Project for Certification. In this project you will

Rajdeep Mondal 1 Dec 23, 2021
the project for the most brutal and effective language learning technique

- "The project for the most brutal and effective language learning technique" (c) Alex Kay The langflow project was created especially for language le

Alexander Kaigorodov 7 Dec 26, 2021
Documentation and issues for Pylance - Fast, feature-rich language support for Python

Documentation and issues for Pylance - Fast, feature-rich language support for Python

Microsoft 1.5k Dec 29, 2022
Coursera learning course Python the basics. Programming exercises and tasks

HSE_Python_the_basics Welcome to BAsics programming Python! You’re joining thousands of learners currently enrolled in the course. I'm excited to have

PavelRyzhkov 0 Jan 05, 2022
Sphinx Theme Builder

Sphinx Theme Builder Streamline the Sphinx theme development workflow, by building upon existing standardised tools. and provide a: simplified packagi

Pradyun Gedam 23 Dec 26, 2022
Materi workshop "Light up your Python!" Himpunan Mahasiswa Sistem Informasi Fakultas Ilmu Komputer Universitas Singaperbangsa Karawang, 4 September 2021 (Online via Zoom).

Workshop Python UNSIKA 2021 Materi workshop "Light up your Python!" Himpunan Mahasiswa Sistem Informasi Fakultas Ilmu Komputer Universitas Singaperban

Eka Putra 20 Mar 24, 2022
Count the number of lines of code in a directory, minus the irrelevant stuff

countloc Simple library to count the lines of code in a directory (excluding stuff like node_modules) Simply just run: countloc node_modules args to

Anish 4 Feb 14, 2022
Gaphor is the simple modeling tool

Gaphor Gaphor is a UML and SysML modeling application written in Python. It is designed to be easy to use, while still being powerful. Gaphor implemen

Gaphor 1.3k Jan 03, 2023
Projeto em Python colaborativo para o Bootcamp de Dados do Itaú em parceria com a Lets Code

🧾 lets-code-todo-list por Henrique V. Domingues e Josué Montalvão Projeto em Python colaborativo para o Bootcamp de Dados do Itaú em parceria com a L

Henrique V. Domingues 1 Jan 11, 2022
NetBox plugin that stores configuration diffs and checks templates compliance

Config Officer - NetBox plugin NetBox plugin that deals with Cisco device configuration (collects running config from Cisco devices, indicates config

77 Dec 21, 2022
Variable Transformer Calculator

✠ VASCO - VAriable tranSformer CalculatOr Software que calcula informações de transformadores feita para a matéria de "Conversão Eletromecânica de Ene

Arthur Cordeiro Andrade 2 Feb 12, 2022
Near Zero-Overhead Python Code Coverage

Slipcover: Near Zero-Overhead Python Code Coverage by Juan Altmayer Pizzorno and Emery Berger at UMass Amherst's PLASMA lab. About Slipcover Slipcover

PLASMA @ UMass 325 Dec 28, 2022