thomaxxl / safrs Goto Github PK
View Code? Open in Web Editor NEWSqlAlchemy Flask-Restful Swagger Json:API OpenAPI
License: GNU General Public License v3.0
SqlAlchemy Flask-Restful Swagger Json:API OpenAPI
License: GNU General Public License v3.0
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?
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
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
Trying to use SAFRS i've structured my App to have separated models, but when i do so I somehow get session ownership issues like the ones described here: https://stackoverflow.com/questions/24291933/sqlalchemy-object-already-attached-to-session
removing the SAFRS constructor from the models yelds no issue in this case.
I've created a minimal example showing the issue here:
https://gist.github.com/xaiki/a993231192b18836c0dfcd1233ef7331
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 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.
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.
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)
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.
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
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.
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.
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.
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 :-)
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'
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.
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'
----------------------------------------
Currently testing the latest release, 8afc35d but the search and re_search now requires an ID
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
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
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)
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?
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"
},
`"""
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).
I modfied the skype_rest for mysql, but it does't work, can you supply a demo for mysql
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
README.md and /examples/README.md hav links to examples/demo.py
, but no files exists.
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)
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
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?
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.
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)
api = SAFRSAPI(app, host=host+":"+str(port), port=port, prefix=api_prefix)
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]
.
It would be nice to be able to require authentication for certain methods (POST/PUT/DELETE) and not others (GET/HEAD).
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
Would it be possible to add support for the geoalchemy2 types?
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.
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?
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
Line 99 in 0ae2615
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:
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 ?
It is possible to connect safrs to another REST API?
Client -> SAFRS -> REST API
The goal is to create a middleware composing responses from different REST APIs into a consolidated SAFRS Api.
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'
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()
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
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)
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.
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
Line 658 in c982bd3
else:
?
or it would even be nicer to have an extra Column attribute for example "sample_value"
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
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?
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'
Hi Thomas - Thanks for the project. Would it be possible to add the templates folder to GitHub? I was running the demo_full.py, but was getting an error with missing my_master.html file when trying to access the admin UI
Getting a copy of https://github.com/flask-admin/flask-admin/blob/master/examples/auth/templates/my_master.html seem to work.
Thanks,
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?
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.