Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# TODO
remaining tasks for the `0.2` release, in order of priority

## major
* [ ] move behaviours (getting, updating, etc.) to library methods
* [ ] validation (schema, column, relationship)
* [ ] declarative entity definitions
* [ ] schema validation
* [ ] documentation
* [ ] callbacks
* [ ] pagination
* [ ] hateoas-style support

### move behaviours
at the moment, when you generate your code, a lot of functionality
gets put in the restplus endpoint. this results in huge diffs between
versions and is tiresome to test.

it is proposed that this code moves into a library function which then
gets imported by the endpoint. this will make the code easier to test,
and will result in a smaller generated codebase.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love it. It would allow people to be much more granular about which bits of the Genyrator they use.


### validation
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sounds lovely, in practice we've seen this being a huge problem in day to day use.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoops, I massively typoed in there. In practice we've not seen this being a huge problem in day to day use. I think it would be nice, I'm not sure it's the highest priority for us.

there's loads that could be done to validate a schema. the most interesting
part is probably the relationships - to do this we would need a two-pass
implementation where the first pass builds up an idea of what entities are
present and how they relate to one another, and the second pass
verifies that these relationships are possible.

error reporting should be as approachable and helpful as possible. see
[elm](https://elm-lang.org/) for inspiration.

### declarative entity definitions
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is hard to get right. There is a lot of additional information involved in putting together a a set of entities and decorator based interfaces are notoriously hard to get right, particularly if they're dealing with a lot of data.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

think this would be a much nicer interface than the current declarative approach. with the decorators I only mean for collecting the classes - the alternative to this is doing it in the SQLAlchemy style which I think is a lot less pleasant. would be fun to spike this out to see how it looks

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe I need to see it. It would be nice if it could be implemented initially as an additional interface that produces the current interface. That way it could introduced in a backwards compatible way.

writing entities declaratively is much more natural to python developers
already familiar with sqlalchemy, which is a dependency of genyrator.
it is proposed to use decorators in the style of
[attr.s](http://www.attrs.org/en/stable/) to configure the behaviour of
entities.

### schema validation
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would strongly encourage against implementing yet another validation engine. There are two (three if you count jsonschema) as part of genyrator already (marshmallow and flask-resplus). I think it would be better if figured out how to lean on one of those more.

Given that flask-restplus seems to be unmaintained now and marshmallow is widely used in the community my preference would be there. Even if it is a bit lacking.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed, would be nice if we could do this with an existing framework 👍

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

marshmallow-sqlalchemy has support for overriding fields and for manually generating fields (that function accepts keyword arguments that are passed on down to the marshmallow field) so this does not sound very difficult.

we currently validate provided data with a really old version
of marhsmallow-sqlalchemy. pros: it gives nice errors and will persist
the data if it's valid. cons: it's not easily configurable from the standpoint
of someone writing an entity. explore going with a different library, writing our
own or something different? there could be the opportunity to allow the option
to specify column-level validation - how would this look?


### callbacks
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be interesting to step back and think of different ways of approaching this. Are callbacks the right answer or being able to fully override certain aspects. What are the actual user needs that we have come across? What would be most natural to a developer that already knows flask-restplus / flask / sqlalchemy / marshmallow ...?

Copy link
Owner Author

@jumblesale jumblesale Mar 6, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had this in here more as a place to stick ideas. the idea was that if you just want to customise a specific endpoint, or do a little more processing on all GETs or something like that, you could sneak your own code in.

provide a mechanism for intercepting the data at different points of processing.
perhaps as a first pass, just allow a user-provided method to be called at the start
of the endpoint method, and another before the response gets serialized?


### documentation

it would be wonderful to have documentation which takes the time to explain
some of the core concepts of columns, entities and schemas, as well as
giving examples of how to configure different kinds of relationships.

### pagination
configurable pagination on a per-endpoint basis.

### hateoas-style support
location headers. explorable links.

## minor

* [ ] stop using `.format`, start using `f` strings everywhere
2 changes: 1 addition & 1 deletion bookshop/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from flask_marshmallow import Marshmallow
from bookshop.resources import api
from bookshop.config import config
from bookshop.sqlalchemy import db
from bookshop.sqlalchemy import db as db

app = config()
db.init_app(app)
Expand Down
2 changes: 1 addition & 1 deletion bookshop/resources/Author.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from bookshop.core.convert_dict import (
python_dict_to_json_dict, json_dict_to_python_dict
)
from bookshop.sqlalchemy import db
from bookshop.sqlalchemy import db as db
from bookshop.sqlalchemy.model import Author
from bookshop.sqlalchemy.convert_properties import (
convert_properties_to_sqlalchemy_properties, convert_sqlalchemy_properties_to_dict_properties
Expand Down
2 changes: 1 addition & 1 deletion bookshop/resources/Book.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from bookshop.core.convert_dict import (
python_dict_to_json_dict, json_dict_to_python_dict
)
from bookshop.sqlalchemy import db
from bookshop.sqlalchemy import db as db
from bookshop.sqlalchemy.model import Book
from bookshop.sqlalchemy.convert_properties import (
convert_properties_to_sqlalchemy_properties, convert_sqlalchemy_properties_to_dict_properties
Expand Down
2 changes: 1 addition & 1 deletion bookshop/resources/BookGenre.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from bookshop.core.convert_dict import (
python_dict_to_json_dict, json_dict_to_python_dict
)
from bookshop.sqlalchemy import db
from bookshop.sqlalchemy import db as db
from bookshop.sqlalchemy.model import BookGenre
from bookshop.sqlalchemy.convert_properties import (
convert_properties_to_sqlalchemy_properties, convert_sqlalchemy_properties_to_dict_properties
Expand Down
2 changes: 1 addition & 1 deletion bookshop/resources/Genre.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from bookshop.core.convert_dict import (
python_dict_to_json_dict, json_dict_to_python_dict
)
from bookshop.sqlalchemy import db
from bookshop.sqlalchemy import db as db
from bookshop.sqlalchemy.model import Genre
from bookshop.sqlalchemy.convert_properties import (
convert_properties_to_sqlalchemy_properties, convert_sqlalchemy_properties_to_dict_properties
Expand Down
2 changes: 1 addition & 1 deletion bookshop/resources/RelatedBook.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from bookshop.core.convert_dict import (
python_dict_to_json_dict, json_dict_to_python_dict
)
from bookshop.sqlalchemy import db
from bookshop.sqlalchemy import db as db
from bookshop.sqlalchemy.model import RelatedBook
from bookshop.sqlalchemy.convert_properties import (
convert_properties_to_sqlalchemy_properties, convert_sqlalchemy_properties_to_dict_properties
Expand Down
2 changes: 1 addition & 1 deletion bookshop/resources/Review.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from bookshop.core.convert_dict import (
python_dict_to_json_dict, json_dict_to_python_dict
)
from bookshop.sqlalchemy import db
from bookshop.sqlalchemy import db as db
from bookshop.sqlalchemy.model import Review
from bookshop.sqlalchemy.convert_properties import (
convert_properties_to_sqlalchemy_properties, convert_sqlalchemy_properties_to_dict_properties
Expand Down
2 changes: 1 addition & 1 deletion bookshop/sqlalchemy/convert_dict_to_marshmallow_result.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from sqlalchemy.ext.declarative import DeclarativeMeta
from sqlalchemy.orm import noload

from bookshop.sqlalchemy import db
from bookshop.sqlalchemy import db as db
from bookshop.core.convert_dict import json_dict_to_python_dict
from bookshop.domain.types import DomainModel, UserJson
from bookshop.sqlalchemy.convert_properties import convert_properties_to_sqlalchemy_properties
Expand Down
2 changes: 1 addition & 1 deletion bookshop/sqlalchemy/fixture/Author.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import factory

from bookshop.sqlalchemy import db
from bookshop.sqlalchemy import db as db
from bookshop.sqlalchemy.model.Author import Author


Expand Down
2 changes: 1 addition & 1 deletion bookshop/sqlalchemy/fixture/Book.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import factory

from bookshop.sqlalchemy import db
from bookshop.sqlalchemy import db as db
from bookshop.sqlalchemy.model.Book import Book


Expand Down
2 changes: 1 addition & 1 deletion bookshop/sqlalchemy/fixture/BookGenre.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import factory

from bookshop.sqlalchemy import db
from bookshop.sqlalchemy import db as db
from bookshop.sqlalchemy.model.BookGenre import BookGenre


Expand Down
2 changes: 1 addition & 1 deletion bookshop/sqlalchemy/fixture/Genre.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import factory

from bookshop.sqlalchemy import db
from bookshop.sqlalchemy import db as db
from bookshop.sqlalchemy.model.Genre import Genre


Expand Down
2 changes: 1 addition & 1 deletion bookshop/sqlalchemy/fixture/RelatedBook.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import factory

from bookshop.sqlalchemy import db
from bookshop.sqlalchemy import db as db
from bookshop.sqlalchemy.model.RelatedBook import RelatedBook


Expand Down
2 changes: 1 addition & 1 deletion bookshop/sqlalchemy/fixture/Review.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import factory

from bookshop.sqlalchemy import db
from bookshop.sqlalchemy import db as db
from bookshop.sqlalchemy.model.Review import Review


Expand Down
2 changes: 1 addition & 1 deletion bookshop/sqlalchemy/model/Author.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from sqlalchemy import UniqueConstraint
from sqlalchemy.types import JSON as JSONType

from bookshop.sqlalchemy import db
from bookshop.sqlalchemy import db as db
from bookshop.sqlalchemy.model.types import BigIntegerVariantType


Expand Down
2 changes: 1 addition & 1 deletion bookshop/sqlalchemy/model/Book.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from sqlalchemy import UniqueConstraint
from sqlalchemy.types import JSON as JSONType

from bookshop.sqlalchemy import db
from bookshop.sqlalchemy import db as db
from bookshop.sqlalchemy.model.types import BigIntegerVariantType


Expand Down
2 changes: 1 addition & 1 deletion bookshop/sqlalchemy/model/BookGenre.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from sqlalchemy import UniqueConstraint
from sqlalchemy.types import JSON as JSONType

from bookshop.sqlalchemy import db
from bookshop.sqlalchemy import db as db
from bookshop.sqlalchemy.model.types import BigIntegerVariantType


Expand Down
2 changes: 1 addition & 1 deletion bookshop/sqlalchemy/model/Genre.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from sqlalchemy import UniqueConstraint
from sqlalchemy.types import JSON as JSONType

from bookshop.sqlalchemy import db
from bookshop.sqlalchemy import db as db
from bookshop.sqlalchemy.model.types import BigIntegerVariantType


Expand Down
2 changes: 1 addition & 1 deletion bookshop/sqlalchemy/model/RelatedBook.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from sqlalchemy import UniqueConstraint
from sqlalchemy.types import JSON as JSONType

from bookshop.sqlalchemy import db
from bookshop.sqlalchemy import db as db
from bookshop.sqlalchemy.model.types import BigIntegerVariantType


Expand Down
2 changes: 1 addition & 1 deletion bookshop/sqlalchemy/model/Review.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from sqlalchemy import UniqueConstraint
from sqlalchemy.types import JSON as JSONType

from bookshop.sqlalchemy import db
from bookshop.sqlalchemy import db as db
from bookshop.sqlalchemy.model.types import BigIntegerVariantType


Expand Down
Empty file added genyrator/behaviour/__init__.py
Empty file.
Empty file.
22 changes: 22 additions & 0 deletions genyrator/behaviour/sqlalchemy/db.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from functools import lru_cache

from flask_sqlalchemy import SQLAlchemy


class DBNotInitialisedException(Exception):
message = "It looks like you're trying to use genyrator without having "
"initialised the db object first! Call `init_genyrator` with your SQLAlchemy "
"db instance before trying to use any of the generated code."


def init_genyrator(
sqlalchemy_instance: SQLAlchemy,
):
get_db_instance._db_instance = sqlalchemy_instance


def get_db_instance():
if not hasattr(get_db_instance, '_db_instance'):
raise DBNotInitialisedException(DBNotInitialisedException.message)
else:
return get_db_instance._db_instance
4 changes: 2 additions & 2 deletions genyrator/entities/File.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ def create_files_from_template_config(
def create_file_from_template(file_path: List[str], template: Template) -> File:
if template.out_path:
file_path = file_path + template.out_path[0]
file_name = '{}.py'.format(template.out_path[1])
file_name = f'{template.out_path[1]}.py'
else:
file_path = file_path + template.relative_path
file_name = '{}.py'.format(template.template_name)
file_name = f'{template.template_name}.py'
return File(
file_name=file_name,
file_path=file_path,
Expand Down
40 changes: 28 additions & 12 deletions genyrator/entities/Schema.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
from typing import List, Optional
from typing import List, Optional, NamedTuple, Tuple
import attr

from genyrator.entities.Entity import Entity
from genyrator.entities.File import create_files_from_template_config, FileList
from genyrator.entities.Template import Template
from genyrator.template_config import create_template_config
from genyrator.template_config import create_template_config, TemplateConfig


@attr.s
class Schema(object):
module_name: str = attr.ib()
entities: List[Entity] = attr.ib()
templates: List[Template] = attr.ib()
files: FileList = attr.ib()
api_name: str = attr.ib()
api_description: str = attr.ib()
module_name: str = attr.ib()
entities: List[Entity] = attr.ib()
templates: TemplateConfig = attr.ib()
files: FileList = attr.ib()
api_name: str = attr.ib()
api_description: str = attr.ib()

def write_files(self) -> None:
for file_list in self.files:
Expand Down Expand Up @@ -48,18 +47,35 @@ def write_domain_models(self):
def create_schema(
module_name: str,
entities: List[Entity],
db_import_path: Optional[str] = None,
db_import: Optional[str] = None,
api_name: Optional[str] = None,
api_description: Optional[str] = None,
file_path: Optional[List[str]] = None,
) -> Schema:
db_import_path = db_import_path if db_import_path else '{}.sqlalchemy'.format(module_name)
"""Create a Genyrator Schema.

A Schema represents a collection of entities, as well as details on how to
write the generated code.

Args:
module_name: The name used for the generated module. Gets used in import statements
entities: A list of Entities to include in the Schema
db_import: A string containing a statement which, when evaluated, will import an SQLAlchemy
db object. Defaults to "from {module_name}.sqlalchemy import db". You should
make this point to your db object if you wish to use a non-generated db
initialization.
api_name: Used in the generation of the RESTPLUS API. Shows up in the generated Swagger
api_description: Used in the generated RESTPLUS API
file_path: The path to where to write the files to. Defaults to the module name - if
the module_name is "bookshop", the app will be written to "bookshop/"
"""
db_import = db_import if db_import else f'from {module_name}.sqlalchemy import db'
file_path = file_path if file_path else [module_name]
api_name = api_name if api_name else module_name
api_description = api_description if api_description else ''
template_config = create_template_config(
module_name=module_name,
db_import_path=db_import_path,
db_import_statement=db_import,
entities=entities,
api_name=api_name,
api_description=api_description,
Expand Down
Loading