Git Product home page Git Product logo

safrs's Introduction

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

SAFRS: Python OpenAPI & JSON:API Framework

demo

Introduction

SAFRS exposes SQLAlchemy database models as a JSON:API webservice and generates the corresponding swagger/OpenAPI.

Documentation can be found in the wiki.

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

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, for example

python 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 expose_object

api.expose_object(User)    

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

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:password@localhost/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. This approach is used by the ApiLogicServer project.

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 Functionality

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"
}

Customization

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

About SAFRS is an acronym for **S**ql**A**lchemy **F**lask-**R**estful **S**wagger. 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.

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](mailto:[email protected]) an email if you run into problems or something isn't clear!
References
Thanks I developed this code when I worked at [Excellium Services](https://www.excellium-services.com/). They allowed me to open source it when I stopped working there.

safrs's People

Contributors

codacy-badger avatar delirious-lettuce avatar dependabot[bot] avatar dwolff avatar imriss avatar janl99 avatar kurianbenoy-aot avatar lapierreni avatar maab avatar michaelkrupp avatar polyatail avatar thomaxxl avatar wicol avatar xaiki avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

safrs's Issues

automap example bug

Hi,

Great package.

Just using the automap feature as in the skype example I noticed that when using the type() call to create the models as a subclass of both SAFRSBase and table there is a small issue. The sclass.query.limit().all() returns instances that are not instances of SAFRSBase - this only bugs when using "include" as there is an assert in the code. I think this is because the "prepare" call on the base happened before the call to type? Anyway i got round it by adding automap_base(cls=SAFRSBase) so that by default the class is a subclass of the base.

Just thought i would let you know,

Simon

cors_domain is not being read

Even if you set the "cors_domain" variable, it is not being read on the jsonapi.py, via globals(). I'm using python 2.7.14

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?

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.

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)

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: #6

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

lazy loading ?

I may be mistaken but I didn't see a way to lazy load the Api object so that it's easily usable with the app factory pattern, is this something achievable ?

Filter records by logged in user

Hi After a user logins and gets a token, how do I filter all Gets to only show records with a relationship to the User record? And Post to only post if the parent relationship key exist?

Thank you

demo_jwt.py -- TypeError: 'module' object is not callable

When I run examples\authentication\demo_jwt.py I get this error: TypeError: 'module' object is not callable How do I resolve this error?

Traceback (most recent call last):
  File "C:\Sites\safrs2\safrs\_api.py", line 468, in api_decorator
    decorated_method = swagger_decorator(decorated_method)
  File "C:\Sites\safrs2\safrs\swagger_doc.py", line 484, in swagger_doc_gen
    rel_post_schema = schema_from_object("{}_Relationship".format(class_name), {"data": data})
  File "C:\Sites\safrs2\safrs\swagger_doc.py", line 198, in schema_from_object
    properties = encode_schema(properties)
  File "C:\Sites\safrs2\safrs\swagger_doc.py", line 154, in encode_schema
    log.warning("Json encoding failed for {}, type {} ({})".format(obj, type(obj), exc))
NameError: name 'log' is not defined
[2020-04-27 12:22:37,736] _api:475 ERROR: Failed to generate documentation for <function SAFRSRestRelationshipAPI.post at 0x051731E0>
[2020-04-27 12:22:37,784] _api:474 ERROR: name 'log' is not defined
<function SAFRSRestRelationshipAPI.delete at 0x051738A0> delete
Traceback (most recent call last):
  File "C:\Sites\safrs2\safrs\swagger_doc.py", line 152, in encode_schema
    result = json.loads(json.dumps(obj, cls=flask.current_app.json_encoder))
  File "c:\users\chris\appdata\local\programs\python\python36-32\Lib\json\__init__.py", line 238, in dumps
    **kw).encode(obj)
TypeError: 'module' object is not callable

pipenv install fails

Was reading #10 and tried multiple ways of installing with pipenv

pipenv install -e git+https://github.com/thomaxxl/safrs.git#egg=master
pipenv install safrs
pipenv install safrs==1.1.0

it always fails at the same point, I'm not sure it's a pipenv bug or the setup.py :

 pipenv --version            
pipenv, version 2018.11.26

my Pipfile

[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true

[dev-packages]
pytest = "*"

[packages]
flask = "*"
flask-sqlalchemy = "*"
psycopg2-binary = "*"
safrs = "==1.1.0"

[requires]
python_version = "3.7"

the error messages, strange as it seems like my pipenv venv doesn't catch the fact I'm using 3.7

pipenv install safrs==1.1.0 
Installing safrs==1.1.0...
✔ Installation Succeeded 
Pipfile.lock (3895a0) out of date, updating to (56688f)...
Locking [dev-packages] dependencies...
✔ Success! 
Locking [packages] dependencies...
✘ Locking Failed! 
Traceback (most recent call last):
  File "/usr/local/lib/python2.7/dist-packages/pipenv/resolver.py", line 126, in <module>
    main()
  File "/usr/local/lib/python2.7/dist-packages/pipenv/resolver.py", line 119, in main
    parsed.requirements_dir, parsed.packages)
  File "/usr/local/lib/python2.7/dist-packages/pipenv/resolver.py", line 85, in _main
    requirements_dir=requirements_dir,
  File "/usr/local/lib/python2.7/dist-packages/pipenv/resolver.py", line 69, in resolve
    req_dir=requirements_dir
  File "/usr/local/lib/python2.7/dist-packages/pipenv/utils.py", line 726, in resolve_deps
    req_dir=req_dir,
  File "/usr/local/lib/python2.7/dist-packages/pipenv/utils.py", line 480, in actually_resolve_deps
    resolved_tree = resolver.resolve()
  File "/usr/local/lib/python2.7/dist-packages/pipenv/utils.py", line 385, in resolve
    results = self.resolver.resolve(max_rounds=environments.PIPENV_MAX_ROUNDS)
  File "/usr/local/lib/python2.7/dist-packages/pipenv/patched/piptools/resolver.py", line 102, in resolve
    has_changed, best_matches = self._resolve_one_round()
  File "/usr/local/lib/python2.7/dist-packages/pipenv/patched/piptools/resolver.py", line 206, in _resolve_one_round
    for dep in self._iter_dependencies(best_match):
  File "/usr/local/lib/python2.7/dist-packages/pipenv/patched/piptools/resolver.py", line 301, in _iter_dependencies
    dependencies = self.repository.get_dependencies(ireq)
  File "/usr/local/lib/python2.7/dist-packages/pipenv/patched/piptools/repositories/pypi.py", line 234, in get_dependencies
    legacy_results = self.get_legacy_dependencies(ireq)
  File "/usr/local/lib/python2.7/dist-packages/pipenv/patched/piptools/repositories/pypi.py", line 426, in get_legacy_dependencies
    results, ireq = self.resolve_reqs(download_dir, ireq, wheel_cache)
  File "/usr/local/lib/python2.7/dist-packages/pipenv/patched/piptools/repositories/pypi.py", line 297, in resolve_reqs
    results = resolver._resolve_one(reqset, ireq)
  File "/usr/local/lib/python2.7/dist-packages/pipenv/patched/notpip/_internal/resolve.py", line 274, in _resolve_one
    self.requires_python = check_dist_requires_python(dist, absorb=False)
  File "/usr/local/lib/python2.7/dist-packages/pipenv/patched/notpip/_internal/utils/packaging.py", line 62, in check_dist_requires_python
    '.'.join(map(str, sys.version_info[:3])),)
pipenv.patched.notpip._internal.exceptions.UnsupportedPythonVersion: safrs requires Python '>=3.0, !=3.0.*, !=3.1.*, !=3.2.*, <4' but the running Python is 2.7.15
File "/usr/local/lib/python2.7/dist-packages/pipenv/resolver.py", line 126, in <module>
    main()
  File "/usr/local/lib/python2.7/dist-packages/pipenv/resolver.py", line 119, in main
    parsed.requirements_dir, parsed.packages)
  File "/usr/local/lib/python2.7/dist-packages/pipenv/resolver.py", line 85, in _main
    requirements_dir=requirements_dir,
  File "/usr/local/lib/python2.7/dist-packages/pipenv/resolver.py", line 69, in resolve
    req_dir=requirements_dir
  File "/usr/local/lib/python2.7/dist-packages/pipenv/utils.py", line 726, in resolve_deps
    req_dir=req_dir,
  File "/usr/local/lib/python2.7/dist-packages/pipenv/utils.py", line 480, in actually_resolve_deps
    resolved_tree = resolver.resolve()
  File "/usr/local/lib/python2.7/dist-packages/pipenv/utils.py", line 385, in resolve
    results = self.resolver.resolve(max_rounds=environments.PIPENV_MAX_ROUNDS)
  File "/usr/local/lib/python2.7/dist-packages/pipenv/patched/piptools/resolver.py", line 102, in resolve
    has_changed, best_matches = self._resolve_one_round()
  File "/usr/local/lib/python2.7/dist-packages/pipenv/patched/piptools/resolver.py", line 206, in _resolve_one_round
    for dep in self._iter_dependencies(best_match):
  File "/usr/local/lib/python2.7/dist-packages/pipenv/patched/piptools/resolver.py", line 301, in _iter_dependencies
    dependencies = self.repository.get_dependencies(ireq)
  File "/usr/local/lib/python2.7/dist-packages/pipenv/patched/piptools/repositories/pypi.py", line 234, in get_dependencies
    legacy_results = self.get_legacy_dependencies(ireq)
  File "/usr/local/lib/python2.7/dist-packages/pipenv/patched/piptools/repositories/pypi.py", line 426, in get_legacy_dependencies
    results, ireq = self.resolve_reqs(download_dir, ireq, wheel_cache)
  File "/usr/local/lib/python2.7/dist-packages/pipenv/patched/piptools/repositories/pypi.py", line 297, in resolve_reqs
    results = resolver._resolve_one(reqset, ireq)
  File "/usr/local/lib/python2.7/dist-packages/pipenv/patched/notpip/_internal/resolve.py", line 274, in _resolve_one
    self.requires_python = check_dist_requires_python(dist, absorb=False)
  File "/usr/local/lib/python2.7/dist-packages/pipenv/patched/notpip/_internal/utils/packaging.py", line 62, in check_dist_requires_python
    '.'.join(map(str, sys.version_info[:3])),)
pipenv.patched.notpip._internal.exceptions.UnsupportedPythonVersion: safrs requires Python '>=3.0, !=3.0.*, !=3.1.*, !=3.2.*, <4' but the running Python is 2.7.15

New versions of pip break setup.py

This is the behavior in pip==18.1 and is I believe what setup.py expects:

>>> from pip._internal.req import parse_requirements
>>> foo = listparse_requirements("requirements.txt", session=False))
>>> foo[0].req
<Requirement('Flask-Cors==3.0.7')>

This is the behavior in pip==20.1:

>>> from pip._internal.req import parse_requirements
>>> foo = list(parse_requirements("requirements.txt", session=False))
>>> foo[0].req
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'ParsedRequirement' object has no attribute 'req'
>>> foo[0].requirement
'Flask-Cors==3.0.7'

"Api object has no attribute _extract_schemas"

Hi there!

Excited about this project, and I appreciate your work.

Attempting to run this on pre-existing sqlalchemy models, I get the following error:

api_1       | Traceback (most recent call last):
api_1       |   File "./swagger.py", line 54, in <module>
api_1       |     create_api(app)
api_1       |   File "./swagger.py", line 26, in create_api
api_1       |     api.expose_object(Market)
api_1       |   File "/usr/local/lib/python3.6/site-packages/safrs/jsonapi.py", line 112, in expose_object
api_1       |     methods = ['GET','POST', 'PUT'])
api_1       |   File "/usr/local/lib/python3.6/site-packages/safrs/jsonapi.py", line 265, in add_resource
api_1       |     operation, definitions_ = self._extract_schemas(operation)
api_1       | AttributeError: 'Api' object has no attribute '_extract_schemas'
api_1       | unable to load app 0 (mountpoint='') (callable not found or import error)
api_1       | *** no app loaded. going in full dynamic mode ***
api_1       | *** uWSGI is running in multiple interpreter mode ***
api_1       | spawned uWSGI worker 1 (and the only) (pid: 13, cores: 1)

This is running against objects that inherit from SAFRSBase.

The code is the same as the example, butwith my own models, and exposing them via expose_object.

In addition, I have tried it with the exact test code, replaced sqlite with postgres, and get the same error.

Add possibility to limit methods

Objects are currently accessable via all methods: ['GET', 'POST', 'PATCH', 'DELETE', 'OPTIONS']

It would be great to limit (some) objects to only GET (for example).

And if this is already possible, than i do miss the documentation. I could not easily find how to do this reading the source code.

Why not use Flask-Restless ?

As stated in their definition "Flask-Restless provides simple generation of ReSTful APIs for database models defined using SQLAlchemy (or Flask-SQLAlchemy)."

So it seems the obvious choice to start building on top of it to add swagger/jsonapi support.
Did you consider it ? If so what advantages did you see in Flask-Restful ?

I have no experience in neither of Flask-Restful or Flask-Restless, so it is not a judgement, I just want to know the motivations behind your choice.

database is locked

when I change sqlite uri to the following

    app.config.update(SQLALCHEMY_DATABASE_URI="sqlite:///test.db", DEBUG=True)

start demo_relationshio.py, I got a error:

D:\Python36\python3.exe D:/PycharmProjects/safrs/examples/demo_relationship.py
D:\Python36\lib\site-packages\flask_sqlalchemy\__init__.py:794: FSADeprecationWarning: SQLALCHEMY_TRACK_MODIFICATIONS adds significant overhead and will be disabled by default in the future.  Set it to True or False to suppress this warning.
  'SQLALCHEMY_TRACK_MODIFICATIONS adds significant overhead and '
[2019-09-27 17:10:28,217] _api:156 INFO: Exposing method User.send_mail on /Users/send_mail, endpoint: api.Users.send_mail
[2019-09-27 17:10:28,228] swagger_doc:380 DEBUG: no documentation for "<function SAFRSRestAPI.delete at 0x0000024D214F36A8>" 
[2019-09-27 17:10:28,239] _api:103 INFO: Exposing User on /Users/, endpoint: api.User
[2019-09-27 17:10:28,240] _api:111 INFO: Exposing Users instances on /Users/<string:UserId>/, endpoint: api.UserId
[2019-09-27 17:10:28,266] _api:217 INFO: Exposing relationship books on /Users/<string:UserId>/books, endpoint: /Users/<string:UserId>/api.books
[2019-09-27 17:10:28,269] _api:241 INFO: Exposing User relationship books on /Users/<string:UserId>/books/<string:BookId>, endpoint: /Users/<string:UserId>/api.booksId
[2019-09-27 17:10:28,283] swagger_doc:380 DEBUG: no documentation for "<function SAFRSRestAPI.delete at 0x0000024D214F36A8>" 
[2019-09-27 17:10:28,296] _api:103 INFO: Exposing Book on /Books/, endpoint: api.Book
[2019-09-27 17:10:28,298] _api:111 INFO: Exposing Books instances on /Books/<string:BookId>/, endpoint: api.BookId
[2019-09-27 17:10:28,338] _api:217 INFO: Exposing relationship user on /Books/<string:BookId>/user, endpoint: /Books/<string:BookId>/api.user
[2019-09-27 17:10:28,340] _api:241 INFO: Exposing Book relationship user on /Books/<string:BookId>/user/<string:UserId>, endpoint: /Books/<string:BookId>/api.userId
Starting API: http://localhost:5000
 * Serving Flask app "SAFRS Demo Application" (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: on
 * Restarting with stat
D:\Python36\lib\site-packages\flask_sqlalchemy\__init__.py:794: FSADeprecationWarning: SQLALCHEMY_TRACK_MODIFICATIONS adds significant overhead and will be disabled by default in the future.  Set it to True or False to suppress this warning.
  'SQLALCHEMY_TRACK_MODIFICATIONS adds significant overhead and '
[2019-09-27 17:10:35,801] errors:69 ERROR: Generic Error: (sqlite3.OperationalError) database is locked [SQL: 'INSERT INTO "Users" (id, name, email) VALUES (?, ?, ?)'] [parameters: ('f3899d67-f1f5-4954-a7d8-31c8b131bfab', 'thomas', 'em@il')] (Background on this error at: http://sqlalche.me/e/e3q8)
Traceback (most recent call last):
  File "D:\Python36\lib\site-packages\sqlalchemy\engine\base.py", line 1193, in _execute_context
    context)
  File "D:\Python36\lib\site-packages\sqlalchemy\engine\default.py", line 509, in do_execute
    cursor.execute(statement, parameters)
sqlite3.OperationalError: database is locked

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "D:\PycharmProjects\safrs\safrs\db.py", line 152, in __init__
    safrs.DB.session.commit()
  File "D:\Python36\lib\site-packages\sqlalchemy\orm\scoping.py", line 153, in do
    return getattr(self.registry(), name)(*args, **kwargs)
  File "D:\Python36\lib\site-packages\sqlalchemy\orm\session.py", line 954, in commit
    self.transaction.commit()
  File "D:\Python36\lib\site-packages\sqlalchemy\orm\session.py", line 467, in commit
    self._prepare_impl()
  File "D:\Python36\lib\site-packages\sqlalchemy\orm\session.py", line 447, in _prepare_impl
    self.session.flush()
  File "D:\Python36\lib\site-packages\sqlalchemy\orm\session.py", line 2313, in flush
    self._flush(objects)
  File "D:\Python36\lib\site-packages\sqlalchemy\orm\session.py", line 2440, in _flush
    transaction.rollback(_capture_exception=True)
  File "D:\Python36\lib\site-packages\sqlalchemy\util\langhelpers.py", line 66, in __exit__
    compat.reraise(exc_type, exc_value, exc_tb)
  File "D:\Python36\lib\site-packages\sqlalchemy\util\compat.py", line 249, in reraise
    raise value
  File "D:\Python36\lib\site-packages\sqlalchemy\orm\session.py", line 2404, in _flush
    flush_context.execute()
  File "D:\Python36\lib\site-packages\sqlalchemy\orm\unitofwork.py", line 395, in execute
    rec.execute(self)
  File "D:\Python36\lib\site-packages\sqlalchemy\orm\unitofwork.py", line 560, in execute
    uow
  File "D:\Python36\lib\site-packages\sqlalchemy\orm\persistence.py", line 181, in save_obj
    mapper, table, insert)
  File "D:\Python36\lib\site-packages\sqlalchemy\orm\persistence.py", line 836, in _emit_insert_statements
    execute(statement, multiparams)
  File "D:\Python36\lib\site-packages\sqlalchemy\engine\base.py", line 948, in execute
    return meth(self, multiparams, params)
  File "D:\Python36\lib\site-packages\sqlalchemy\sql\elements.py", line 269, in _execute_on_connection
    return connection._execute_clauseelement(self, multiparams, params)
  File "D:\Python36\lib\site-packages\sqlalchemy\engine\base.py", line 1060, in _execute_clauseelement
    compiled_sql, distilled_params
  File "D:\Python36\lib\site-packages\sqlalchemy\engine\base.py", line 1200, in _execute_context
    context)
  File "D:\Python36\lib\site-packages\sqlalchemy\engine\base.py", line 1413, in _handle_dbapi_exception
    exc_info
  File "D:\Python36\lib\site-packages\sqlalchemy\util\compat.py", line 265, in raise_from_cause
    reraise(type(exception), exception, tb=exc_tb, cause=cause)
  File "D:\Python36\lib\site-packages\sqlalchemy\util\compat.py", line 248, in reraise
    raise value.with_traceback(tb)
  File "D:\Python36\lib\site-packages\sqlalchemy\engine\base.py", line 1193, in _execute_context
    context)
  File "D:\Python36\lib\site-packages\sqlalchemy\engine\default.py", line 509, in do_execute
    cursor.execute(statement, parameters)
sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) database is locked [SQL: 'INSERT INTO "Users" (id, name, email) VALUES (?, ?, ?)'] [parameters: ('f3899d67-f1f5-4954-a7d8-31c8b131bfab', 'thomas', 'em@il')] (Background on this error at: http://sqlalche.me/e/e3q8)

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "D:/PycharmProjects/safrs/examples/demo_relationship.py", line 78, in <module>
    user = User(name="thomas", email="em@il")
  File "<string>", line 4, in __init__
  File "D:\Python36\lib\site-packages\sqlalchemy\orm\state.py", line 424, in _initialize_instance
    manager.dispatch.init_failure(self, args, kwargs)
  File "D:\Python36\lib\site-packages\sqlalchemy\util\langhelpers.py", line 66, in __exit__
    compat.reraise(exc_type, exc_value, exc_tb)
  File "D:\Python36\lib\site-packages\sqlalchemy\util\compat.py", line 249, in reraise
    raise value
  File "D:\Python36\lib\site-packages\sqlalchemy\orm\state.py", line 421, in _initialize_instance
    return manager.original_init(*mixed[1:], **kwargs)
  File "D:\PycharmProjects\safrs\safrs\db.py", line 155, in __init__
    raise GenericError(exc)
safrs.errors.GenericError

returning relations

Is it possible to return, for example, the authors of the book, out of the box?

Lets say I query the books and I would like to display the authors as well, currently this leaves me with 2 options, 1 query all authors or 2 query them individually.

class Book(Model):
    """Book"""
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(255))
    sortable_title = db.Column(db.String(255))
    language = db.Column(db.String(3), comment="ISO-639-2")
    editions = db.relationship("BookEdition", uselist=False, back_populates="book")
    authors = db.relationship(
        "Author",
        secondary=author_book_table,
        back_populates="books",
        lazy='dynamic'
    )
"relationships": {
        "authors": {
          "data": [
            {
              "id": 1,
              "type": "author"
            },
            {
              "id": 2,
              "type": "author"
            },
            {
              "id": 3,
              "type": "author"
            }
          ],
          "links": {
            "self": "http://localhost:5000/book/1/authors"
          },

Data in resource linkage objects for relationships is null

Hi! I am loving your project here. I am using it extensively and find it massively useful, so kudos to you!

I am wondering if you have some guidance here. I have an ORM generated from flask-sqlacodegen, including relationships with backrefs.

When I am querying on the base object, I do see the relationships appear like so:

"relationships": {
                "case_extendeds": {
                    "data": null,
                    "links": {
                        "self": "http://127.0.0.1:5004/cases/3/case_extendeds"
                    }
                },

but the data is null, despite having a valid link that returns the data I need.

My understanding is, the data that I can get with a get request should be provided in the data object, no?

I found the json api specification indicating:

Resource linkage MUST be represented as one of the following:

null for empty to-one relationships.
an empty array ([]) for empty to-many relationships.
a single resource identifier object for non-empty to-one relationships.
an array of resource identifier objects for non-empty to-many relationships

So it looks like this one is saying its an empty to one relationship.

Is there something I'm supposed to provide to actually have it give me the data in the in the data object, or do you have a hunch about something that might be wrong in my setup?

Thanks!

How to add sample value (column default= is not working)

When I try to add sample value through the default value of a field this sample value isn't shown in the example block POST body. See screen print.
no_sample_value_in_experiment_ name

My model Experiment looks like this:
`
class Experiment(SAFRSBase, db.Model):
"""
description: Experiment
"""

__tablename__ = "experiments"
exclude_rels = ["datafile"]
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String, default="Experiment 1")
description = db.Column(db.String, default="")

`

May be here

try:
should be an else: ?

or it would even be nicer to have an extra Column attribute for example "sample_value"

demo.py: TypeError: unsupported operand type(s) for +: 'SQLAlchemy' and 'str'

safrs-1.0.22 / Python 3.7.0 / macOS Mojave 10.14

$ python3 -m venv env
$ source env/bin/activate
$ pip install safrs
$ curl -O https://raw.githubusercontent.com/thomaxxl/safrs/master/examples/demo.py
$ python demo.py

This fails with

Traceback (most recent call last):
  File "demo.py", line 64, in <module>
    SAFRS(app, db)
  File "/blah/blah/env/lib/python3.7/site-packages/safrs/__init__.py", line 37, in __new__
    app.register_blueprint(swaggerui_blueprint, url_prefix= prefix)
  File "/blah/blah/env/lib/python3.7/site-packages/flask/app.py", line 64, in wrapper_func
    return f(self, *args, **kwargs)
  File "/blah/blah/env/lib/python3.7/site-packages/flask/app.py", line 951, in register_blueprint
    blueprint.register(self, options, first_registration)
  File "/blah/blah/env/lib/python3.7/site-packages/flask/blueprints.py", line 151, in register
    endpoint='static')
  File "/blah/blah/env/lib/python3.7/site-packages/flask/blueprints.py", line 68, in add_url_rule
    rule = self.url_prefix + rule
TypeError: unsupported operand type(s) for +: 'SQLAlchemy' and 'str'

Error when installing with pip and pipenv

Trying to install the package using pipenv with pipenv install safrs or the same with pip, this ertror message is displayed:

Collecting safrs
  Downloading https://files.pythonhosted.org/packages/00/21/d6b45459525d75a2a314c35ca01005f509a60ef0edf6124a72abca06cdca/safrs-1.0.15.tar.gz
    Complete output from command python setup.py egg_info:
    Traceback (most recent call last):
      File "<string>", line 1, in <module>
      File "/tmp/pip-install-egxf1ylx/safrs/setup.py", line 17, in <module>
        long_description=open('README.rst').read(),
    FileNotFoundError: [Errno 2] No such file or directory: 'README.rst'

SAFRSRestRelationshipAPI returns all objects in db of the related type

Hi, I just noticed /<obj_type>/<obj_id>/<child_obj_type> returns all instances of child_obj_type in my database. It seems a correct subset of related objects are present on line 853 here, but then an unfiltered query of the related object is fetched from jsonapi_filter:

https://github.com/thomaxxl/safrs/blob/master/safrs/jsonapi.py#L853-L855

I'm guessing the intention was to have filtering support for the related objects. If that's the case then perhaps jsonapi_filter and SAFRSBase._s_filter could take an optional argument for a prepared query that is to be filtered?

swagger issue

Hi,

I noticed that the swagger json created has issues in the relations example.
e.g.
$ref: '#/definitions/Book POST sample'

This has the following error (to see this use http://editor.swagger.io/):
$ref values must be RFC3986-compliant percent-encoded URIs

I will have a look at fixing and then open a pull request.

Simon

The http_method restriction does not apply to a model's relationships

Restrict methods on a model

class Item(...):
    http_methods = ["get"]

    instance = db.relationship("Instance")

class Instance(...):
    http_methods = ["get"]

Now only GET methods should be displayed for Item and Instance.

curl https://myhost.com/api/

Only GET methods should be displayed Item and Instance, but the API page shows item/<id>/instance shows [POST, GET, PATCH, DELETE].

simple flask endpoint

How can I use swagger_doc to document a simple flask app? What I am trying:

@jsonapi_rpc(http_methods=["GET"])
@app.route("/hello/<name>", methods=["GET"])
def hello(name):
	"""
        pageable: True
        description : Hello world
        args:
            name: greeters name
    """
	return make_response("Hello World from %s!"%name, 201)

api = SAFRSAPI(app, host="0.0.0.0", port=5000, prefix="/swagger")

The resulting json has the metadata, but no paths.

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

Hybrid attributes

Unable to use hybrid properties, regular properties do work

https://docs.sqlalchemy.org/en/latest/orm/extensions/hybrid.html

Traceback (most recent call last):
  File "manage.py", line 9, in <module>
    app = init(get_config())
  File "/home/patrick/PycharmProjects/ebookhub-backend/eBookHub/__init__.py", line 47, in init
    configure_blueprints(app)
  File "/home/patrick/PycharmProjects/ebookhub-backend/eBookHub/__init__.py", line 56, in configure_blueprints
    create_api(app)
  File "/home/patrick/PycharmProjects/ebookhub-backend/eBookHub/__init__.py", line 74, in create_api
    api.expose_object(BookEditionFile)
  File "/home/patrick/PycharmProjects/ebookhub-backend/venv/lib/python3.6/site-packages/safrs/_api.py", line 85, in expose_object
    self.expose_methods(url_prefix, tags=tags)
  File "/home/patrick/PycharmProjects/ebookhub-backend/venv/lib/python3.6/site-packages/safrs/_api.py", line 130, in expose_methods
    api_methods = safrs_object.get_jsonapi_rpc_methods()
  File "/home/patrick/PycharmProjects/ebookhub-backend/venv/lib/python3.6/site-packages/safrs/db.py", line 626, in get_documented_api_methods
    for name, method in inspect.getmembers(cls):
  File "/usr/lib/python3.6/inspect.py", line 342, in getmembers
    value = getattr(object, key)
  File "/home/patrick/PycharmProjects/ebookhub-backend/venv/lib/python3.6/site-packages/sqlalchemy/ext/hybrid.py", line 867, in __get__
    return self._expr_comparator(owner)
  File "/home/patrick/PycharmProjects/ebookhub-backend/venv/lib/python3.6/site-packages/sqlalchemy/ext/hybrid.py", line 1066, in expr_comparator
    owner, self.__name__, self, comparator(owner),
  File "/home/patrick/PycharmProjects/ebookhub-backend/venv/lib/python3.6/site-packages/sqlalchemy/ext/hybrid.py", line 1055, in _expr
    return ExprComparator(cls, expr(cls), self)
  File "/home/patrick/PycharmProjects/ebookhub-backend/eBookHub/models/core.py", line 148, in get_path
    return os.path.join(get_temp_path(), self.filename)
  File "/usr/lib/python3.6/posixpath.py", line 94, in join
    genericpath._check_arg_types('join', a, *p)
  File "/usr/lib/python3.6/genericpath.py", line 149, in _check_arg_types
    (funcname, s.__class__.__name__)) from None
TypeError: join() argument must be str or bytes, not 'InstrumentedAttribute'
class BookEditionFile(Model):

    ...

    _result = db.Column(db.Text)
    
    ...

    @hybrid_property
    def result(self):
        if self._result:
            return json.loads(self._result)
        return None

    @result.setter
    def result(self, value):
        self._result = json.dumps(value)

Paging state variables always 0

The function safrs.jsonapi.paginate always returns

      "links": {
        "first": 0,
        "last": 0,
        "next": 0,
        "prev": 0
      },

(hardcoded)
so we don't know if we have other pages or if we reached the last one.

Is it possible to improve this paging function?

Search now requires ID

Currently testing the latest release, 8afc35d but the search and re_search now requires an ID

image

Before extending on the SAFRSBase class I do the following;

from safrs import SAFRSBase
from safrs.api_methods import search, re_search

SAFRSBase.search = search
SAFRSBase.re_search = re_search

Subclassing db.Model

from Limitations & TODOs:

I am not a big fan of the multiple inheritance needed to declare SAFRSBase instances but I couldn't subclass sqla's db.Model and I think inheritance is more clear than class decorators.

This is actually pretty easy.
All you need to do is pass your custom class to the constructor of SQLAlchemy:

from flask_sqlalchemy import SQLAlchemy
from flask_sqlalchemy.model import Model
from sqlalchemy.ext.declarative import declarative_base
from safrs import SAFRSBase

MODEL_NAME='Model'

class SAFRSModel(SAFRSBase, Model):
  pass

db = SQLAlchemy(model_class=declarative_base(cls=SAFRSModel, name=MODEL_NAME))

class Users(db.Model):
  __tablename__ = 'users'

You could even go so far and automatically expose SAFRSBase models via a custom Metaclass:

from flask_sqlalchemy import SQLAlchemy
from flask_sqlalchemy.model import Model, DefaultMeta
from sqlalchemy.ext.declarative import declarative_base
from safrs import SAFRSBase, Api

MODEL_NAME='Model'

api = Api(...)

class SAFRSModel(SAFRSBase, Model):
  pass

class SAFRSMeta(DefaultMeta):
  def __init__(cls, name, bases, d):
    if name != MODEL_NAME and issubclass(cls, SAFRSModel):
      api.expose_object(cls)
    super(_DbMeta, cls).__init__(name, bases, d)

db = SQLAlchemy(model_class=declarative_base(cls=SAFRSModel, name=MODEL_NAME))

class Users(db.Model):
  __tablename__ = 'users'

In case you are interested, let me know and I'll put up a PR when I find some free time :-)

Using With Flask-Jwt-Extended

Hello,

I find your framework very helpful and open to development. I wonder, if it works with flask-jwt-extended or not. Since aim of your framework is creating endpoints using sqla models and flask-jwt-extended works with decorators, do you think that entegration of these two framework is possible or not? If so, I'll give a try coz I really need a handy framework like safrs.

Thanks for your effort.

disable `POST /model/{modelId}/`

Is there an easy way to disable the post with the model ID? i.e. POST /model/{modelId}/ since the ID is autoincrement I don't want to let the api client be able to post/create to a certain ID/number.

P.s. sorry for the amount of issues.

swagger.json not found

Hi,

great project. Letting the models get created from an existing database and accessing them through REST calls work, but the Swagger doc is not being created? swagger.json can't be found.

Best regards,
David

static method of user model requires user_id

I would expect this not to be the case since it is a staticmethod. I looked at the search option, see below which one I mean. This is build on a classmethod.

@classmethod
@jsonapi_rpc(http_methods=['POST'])
def search(cls, **kwargs):

my code

class User(Model):
    custom_decorators = [jwt_required, auth.login_required]
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(255))
    email = db.Column(db.String(80, collation='NOCASE'), unique=True, nullable=False)
    password_hash = db.Column(db.String(128))
    language = db.Column(db.String(5), nullable=True, default="en")
    timezone = db.Column(db.String(64), nullable=True, default="Europe/Amsterdam")

    @staticmethod
    @jsonapi_rpc(http_methods=['POST'])
    def login(email_or_token, password):
        user = User.verify_auth_token(email_or_token)
        if not user:
            user = User.query.filter_by(email=email_or_token).first()
            if not user or not user.verify_password(password):
                return None
        return user.create_access_token()

image

about safrs/db.py when column type is datetime/date , format Error.

I use datetime format is '%Y-%m-%d %H:%M:%S' and when value is None , it can't work.

 # Parse datetime and date values
    try:
        if column.type.python_type == datetime.datetime:
            arg_value = datetime.datetime.strptime(str(arg_value), '%Y-%m-%d %H:%M:%S.%f')
        elif column.type.python_type == datetime.date:
            arg_value = datetime.datetime.strptime(str(arg_value), '%Y-%m-%d')
    except (NotImplementedError, ValueError) as exc:
        safrs.log.warning('datetime {} for value "{}"'.format(exc, arg_value))

    return arg_value

And i want to change the code like this:

 # Parse datetime and date values
     if arg_value is not None:
         try:
            if column.type.python_type == datetime.datetime:
                arg_value = datetime.datetime.strptime(str(arg_value), '%Y-%m-%d %H:%M:%S')
            elif column.type.python_type == datetime.date:
                arg_value = datetime.datetime.strptime(str(arg_value), '%Y-%m-%d')
        except (NotImplementedError, ValueError) as exc:
            safrs.log.warning('datetime {} for value "{}"'.format(exc, arg_value))

    return arg_value

Wrong creation of "empty" object on initalization

I've found out that this "try" block was causing a small problem: creating an empty object (just with an id).

After commenting it, the problem was gone. Is it really necessary?

Also:

  • the examples requires a logging import that is not explicit on the examples files. (look demo.py)
  • why do the primary keys have to be a uuid (string)? Can't it be an integer (what is very common on pre-existing databases)

How to not display relationships in the object

Love the project, it helped bootstrap very quickly!

I'd like to be able to an object and all its relationships in the same object.
Why expose the implementation detail to the API user by defining the relationship?

Can't use safrs with poetry

For some reason, poetry does not sees the correct dependencies of the safrs library.
"An error occurred when reading setup.py or setup.cfg: 'Attribute' object has no attribute 'id'" -> this message is shown when I do a "poetry install", having "safrs" as a dependency.

Apparently, poetry can't see what libraries safrs depends on.

pypi version safrs==1.0.10 does not support pip 10, master code does

Collecting safrs==1.0.10 (from x)
  Using cached https://files.pythonhosted.org/packages/7b/05/abc1846dcd495d08c734c61244077f596c39d9036af47304e3f24777f7dd/safrs-1.0.10.tar.gz
    Complete output from command python setup.py egg_info:
    Traceback (most recent call last):
      File "<string>", line 1, in <module>
      File "C:\Users\denny\AppData\Local\Temp\pip-install-u55tif5y\safrs\setup.py", line 4, in <module>
        from pip.req import parse_requirements
    ModuleNotFoundError: No module named 'pip.req'

    ----------------------------------------

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)

Wrong .tar.gz file in setup.py on PyPI

`"""
python3 setup.py sdist
twine upload dist/*
"""

from distutils.core import setup

try: # for pip >= 10
from pip._internal.req import parse_requirements
except ImportError: # for pip <= 9.0.3
from pip.req import parse_requirements

install_requires = [
str(ir.req) for ir in parse_requirements("requirements.txt", session=False)
]

setup(
name="safrs",
packages=["safrs"],
version="2.6.2",
license="MIT",
description="safrs : SqlAlchemy Flask-Restful Swagger2",
long_description=open("README.rst").read(),
author="Thomas Pollet",
author_email="[email protected]",
url="https://github.com/thomaxxl/safrs",
download_url="https://github.com/thomaxxl/safrs/archive/2.5.5.tar.gz",
keywords=["SqlAlchemy", "Flask", "REST", "Swagger", "JsonAPI", "OpenAPI"],
python_requires=">=3.0, !=3.0., !=3.1., !=3.2.*, <4",
install_requires=install_requires,
classifiers=[
"Development Status :: 3 - Alpha",
"License :: OSI Approved :: MIT License",
"Intended Audience :: Developers",
"Framework :: Flask",
"Topic :: Software Development :: Libraries",
"Environment :: Web Environment",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.3",
"Programming Language :: Python :: 3.4",
],
)`

In the "download_url" item, it should point to the correct latest version (2.6.2).

expose_existing.py - line 90, NameError: name 'io' is not defined

File "...safrs-master\expose_existing\expose_existing.py", line 90, in codegen
outfile = io.open(args.outfile, 'w', encoding='utf-8') if args.outfile else capture # sys.stdout
NameError: name 'io' is not defined

Passing an --outfile parameter triggers this error.

Port not included in swagger interface

I'm not sure wetter or not I'm forgetting something or this is an actual bug... If I don't add the port to the host it "forgets" to add it in the URL.

host = "localhost"
port = 5000

api = SAFRSAPI(app, host=host, port=port, prefix=api_prefix)

image

api = SAFRSAPI(app, host=host+":"+str(port), port=port, prefix=api_prefix)

image

Bulk create to endpoints

Hi, this is a very good tool to build the API quickly and I am happy to use it.
Is there a way to bulk create rows?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.