From 590a5eebaf6d32a524103a406f3a673fd5923cfa Mon Sep 17 00:00:00 2001 From: jumblesale Date: Tue, 26 Feb 2019 17:28:46 +0100 Subject: [PATCH 01/10] added TODO.md to track desired elements of the 0.2 release --- TODO.md | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 TODO.md diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..a80de45 --- /dev/null +++ b/TODO.md @@ -0,0 +1,39 @@ +# TODO +remaining tasks for the `0.2` release, in order of priority + +* [ ] move behaviours (getting, updating, etc.) to library methods +* [ ] validation (schema, column, relationship) +* [ ] declarative entity definitions +* [ ] documentation + +## 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. + +## validation +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 +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. + +## 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. From 41c9a14a56b47dfa127adbcd24e7734e2a0578cc Mon Sep 17 00:00:00 2001 From: jumblesale Date: Tue, 26 Feb 2019 17:54:28 +0100 Subject: [PATCH 02/10] added more TODOs --- TODO.md | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/TODO.md b/TODO.md index a80de45..99480c6 100644 --- a/TODO.md +++ b/TODO.md @@ -4,7 +4,11 @@ remaining tasks for the `0.2` release, in order of priority * [ ] 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 @@ -32,8 +36,29 @@ 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 +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 +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. +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. From 18eca4a359f0f11077f3ac5d55994c117f9ccc4e Mon Sep 17 00:00:00 2001 From: jumblesale Date: Mon, 4 Mar 2019 10:25:59 +0100 Subject: [PATCH 03/10] added doc block to create_schema --- genyrator/entities/Schema.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/genyrator/entities/Schema.py b/genyrator/entities/Schema.py index ef2b7de..a5a5c5e 100644 --- a/genyrator/entities/Schema.py +++ b/genyrator/entities/Schema.py @@ -53,6 +53,20 @@ def create_schema( api_description: Optional[str] = None, file_path: Optional[List[str]] = None, ) -> Schema: + """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_path: + 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_path = db_import_path if db_import_path else '{}.sqlalchemy'.format(module_name) file_path = file_path if file_path else [module_name] api_name = api_name if api_name else module_name From 2d99e2777404f07fda4f5e718d3d6ecb125d6960 Mon Sep 17 00:00:00 2001 From: jumblesale Date: Mon, 4 Mar 2019 12:29:08 +0100 Subject: [PATCH 04/10] Added a DBImport type so that we can lazily pull in the db object in the future MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * moved "db_import_path" string to "db_import_statement" * added spec for crerating schema with a DBImport * this is a backwards-compatability breaking interface change 😱 --- bookshop/__init__.py | 2 +- bookshop/resources/Author.py | 2 +- bookshop/resources/Book.py | 2 +- bookshop/resources/BookGenre.py | 2 +- bookshop/resources/Genre.py | 2 +- bookshop/resources/RelatedBook.py | 2 +- bookshop/resources/Review.py | 2 +- .../convert_dict_to_marshmallow_result.py | 2 +- bookshop/sqlalchemy/fixture/Author.py | 2 +- bookshop/sqlalchemy/fixture/Book.py | 2 +- bookshop/sqlalchemy/fixture/BookGenre.py | 2 +- bookshop/sqlalchemy/fixture/Genre.py | 2 +- bookshop/sqlalchemy/fixture/RelatedBook.py | 2 +- bookshop/sqlalchemy/fixture/Review.py | 2 +- bookshop/sqlalchemy/model/Author.py | 2 +- bookshop/sqlalchemy/model/Book.py | 2 +- bookshop/sqlalchemy/model/BookGenre.py | 2 +- bookshop/sqlalchemy/model/Genre.py | 2 +- bookshop/sqlalchemy/model/RelatedBook.py | 2 +- bookshop/sqlalchemy/model/Review.py | 2 +- genyrator/entities/Schema.py | 32 ++++++++++------- genyrator/entities/Template.py | 34 +++++++++---------- genyrator/template_config.py | 22 ++++++------ genyrator/templates/__init__.j2 | 2 +- genyrator/templates/resources/resource.j2 | 2 +- .../convert_dict_to_marshmallow_result.j2 | 2 +- .../templates/sqlalchemy/fixture/fixture.j2 | 2 +- .../sqlalchemy/model/sqlalchemy_model.j2 | 2 +- test/spec/genyrator/entities/Schema_spec.py | 16 +++++++++ 29 files changed, 88 insertions(+), 66 deletions(-) create mode 100644 test/spec/genyrator/entities/Schema_spec.py diff --git a/bookshop/__init__.py b/bookshop/__init__.py index 02f6d8e..a5f9478 100644 --- a/bookshop/__init__.py +++ b/bookshop/__init__.py @@ -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) diff --git a/bookshop/resources/Author.py b/bookshop/resources/Author.py index bf38ea7..f9cf79b 100644 --- a/bookshop/resources/Author.py +++ b/bookshop/resources/Author.py @@ -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 diff --git a/bookshop/resources/Book.py b/bookshop/resources/Book.py index 6a9951d..cd5f84f 100644 --- a/bookshop/resources/Book.py +++ b/bookshop/resources/Book.py @@ -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 diff --git a/bookshop/resources/BookGenre.py b/bookshop/resources/BookGenre.py index eced0c4..86a61b2 100644 --- a/bookshop/resources/BookGenre.py +++ b/bookshop/resources/BookGenre.py @@ -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 diff --git a/bookshop/resources/Genre.py b/bookshop/resources/Genre.py index 14f85ee..45ce3fa 100644 --- a/bookshop/resources/Genre.py +++ b/bookshop/resources/Genre.py @@ -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 diff --git a/bookshop/resources/RelatedBook.py b/bookshop/resources/RelatedBook.py index c26d73b..e451e07 100644 --- a/bookshop/resources/RelatedBook.py +++ b/bookshop/resources/RelatedBook.py @@ -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 diff --git a/bookshop/resources/Review.py b/bookshop/resources/Review.py index 2699119..22959bc 100644 --- a/bookshop/resources/Review.py +++ b/bookshop/resources/Review.py @@ -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 diff --git a/bookshop/sqlalchemy/convert_dict_to_marshmallow_result.py b/bookshop/sqlalchemy/convert_dict_to_marshmallow_result.py index 18cf42d..2a4745d 100644 --- a/bookshop/sqlalchemy/convert_dict_to_marshmallow_result.py +++ b/bookshop/sqlalchemy/convert_dict_to_marshmallow_result.py @@ -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 diff --git a/bookshop/sqlalchemy/fixture/Author.py b/bookshop/sqlalchemy/fixture/Author.py index ec17e11..687837f 100644 --- a/bookshop/sqlalchemy/fixture/Author.py +++ b/bookshop/sqlalchemy/fixture/Author.py @@ -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 diff --git a/bookshop/sqlalchemy/fixture/Book.py b/bookshop/sqlalchemy/fixture/Book.py index f3855e7..2acdb85 100644 --- a/bookshop/sqlalchemy/fixture/Book.py +++ b/bookshop/sqlalchemy/fixture/Book.py @@ -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 diff --git a/bookshop/sqlalchemy/fixture/BookGenre.py b/bookshop/sqlalchemy/fixture/BookGenre.py index 4c04994..4862c15 100644 --- a/bookshop/sqlalchemy/fixture/BookGenre.py +++ b/bookshop/sqlalchemy/fixture/BookGenre.py @@ -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 diff --git a/bookshop/sqlalchemy/fixture/Genre.py b/bookshop/sqlalchemy/fixture/Genre.py index 3452fa7..574833d 100644 --- a/bookshop/sqlalchemy/fixture/Genre.py +++ b/bookshop/sqlalchemy/fixture/Genre.py @@ -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 diff --git a/bookshop/sqlalchemy/fixture/RelatedBook.py b/bookshop/sqlalchemy/fixture/RelatedBook.py index 9f6c0f7..e0cb3f6 100644 --- a/bookshop/sqlalchemy/fixture/RelatedBook.py +++ b/bookshop/sqlalchemy/fixture/RelatedBook.py @@ -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 diff --git a/bookshop/sqlalchemy/fixture/Review.py b/bookshop/sqlalchemy/fixture/Review.py index abacd37..bd10557 100644 --- a/bookshop/sqlalchemy/fixture/Review.py +++ b/bookshop/sqlalchemy/fixture/Review.py @@ -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 diff --git a/bookshop/sqlalchemy/model/Author.py b/bookshop/sqlalchemy/model/Author.py index 65cf674..30814fb 100644 --- a/bookshop/sqlalchemy/model/Author.py +++ b/bookshop/sqlalchemy/model/Author.py @@ -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 diff --git a/bookshop/sqlalchemy/model/Book.py b/bookshop/sqlalchemy/model/Book.py index b5cf7c1..c290701 100644 --- a/bookshop/sqlalchemy/model/Book.py +++ b/bookshop/sqlalchemy/model/Book.py @@ -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 diff --git a/bookshop/sqlalchemy/model/BookGenre.py b/bookshop/sqlalchemy/model/BookGenre.py index 5699a9b..99f6c0c 100644 --- a/bookshop/sqlalchemy/model/BookGenre.py +++ b/bookshop/sqlalchemy/model/BookGenre.py @@ -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 diff --git a/bookshop/sqlalchemy/model/Genre.py b/bookshop/sqlalchemy/model/Genre.py index 9c70394..96f6a7e 100644 --- a/bookshop/sqlalchemy/model/Genre.py +++ b/bookshop/sqlalchemy/model/Genre.py @@ -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 diff --git a/bookshop/sqlalchemy/model/RelatedBook.py b/bookshop/sqlalchemy/model/RelatedBook.py index 9b18dd1..2f098b5 100644 --- a/bookshop/sqlalchemy/model/RelatedBook.py +++ b/bookshop/sqlalchemy/model/RelatedBook.py @@ -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 diff --git a/bookshop/sqlalchemy/model/Review.py b/bookshop/sqlalchemy/model/Review.py index 2e37ea8..285c64e 100644 --- a/bookshop/sqlalchemy/model/Review.py +++ b/bookshop/sqlalchemy/model/Review.py @@ -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 diff --git a/genyrator/entities/Schema.py b/genyrator/entities/Schema.py index a5a5c5e..dc966b0 100644 --- a/genyrator/entities/Schema.py +++ b/genyrator/entities/Schema.py @@ -1,20 +1,24 @@ -from typing import List, Optional +from typing import List, Optional, NamedTuple 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 + +DBImport = NamedTuple('DBImport', + [('db_module', str), + ('db_variable_name', str), ] +) @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: @@ -48,7 +52,7 @@ def write_domain_models(self): def create_schema( module_name: str, entities: List[Entity], - db_import_path: Optional[str] = None, + db_import: Optional[DBImport] = None, api_name: Optional[str] = None, api_description: Optional[str] = None, file_path: Optional[List[str]] = None, @@ -61,19 +65,21 @@ def create_schema( 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_path: + db_import: A DBImport describing how to access the sqlalchemy database object used by + the app. Defaults to importing "db" from "{module_name}.sqlalchemy" 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_path = db_import_path if db_import_path else '{}.sqlalchemy'.format(module_name) + db_import = db_import if db_import else DBImport(f'{module_name}.sqlalchemy', 'db') + db_import_statement = f'from {db_import.db_module} import {db_import.db_variable_name} as 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_statement, entities=entities, api_name=api_name, api_description=api_description, diff --git a/genyrator/entities/Template.py b/genyrator/entities/Template.py index d9e62b2..05c6c9f 100644 --- a/genyrator/entities/Template.py +++ b/genyrator/entities/Template.py @@ -52,8 +52,8 @@ def create_template( @attr.s class RootInit(Template): - db_import_path: str = attr.ib() - module_name: str = attr.ib() + db_import_statement: str = attr.ib() + module_name: str = attr.ib() @attr.s @@ -69,9 +69,9 @@ class ConvertDict(Template): @attr.s class SQLAlchemyModel(Template): - module_name: str = attr.ib() - db_import_path: str = attr.ib() - entity: Entity = attr.ib() + module_name: str = attr.ib() + db_import_statement: str = attr.ib() + entity: Entity = attr.ib() @attr.s @@ -86,8 +86,8 @@ class Config(Template): @attr.s class SQLAlchemyModelInit(Template): - module_name: str = attr.ib() - db_import_path: str = attr.ib() + module_name: str = attr.ib() + db_import_statement: str = attr.ib() imports: List[Import] = attr.ib() @@ -98,11 +98,11 @@ class RestplusModel(Template): @attr.s class Resource(Template): - module_name: str = attr.ib() - db_import_path: str = attr.ib() - entity: Entity = attr.ib() - restplus_template: str = attr.ib() - TypeOption: Type = attr.ib() + module_name: str = attr.ib() + db_import_statement: str = attr.ib() + entity: Entity = attr.ib() + restplus_template: str = attr.ib() + TypeOption: Type = attr.ib() @attr.s @@ -142,12 +142,12 @@ class JoinEntities(Template): @attr.s class ConvertDictToMarshmallow(Template): - module_name: str = attr.ib() - db_import_path: str = attr.ib() + module_name: str = attr.ib() + db_import_statement: str = attr.ib() @attr.s class Fixture(Template): - db_import_path: str = attr.ib() - module_name: str = attr.ib() - entity: Entity = attr.ib() + db_import_statement: str = attr.ib() + module_name: str = attr.ib() + entity: Entity = attr.ib() diff --git a/genyrator/template_config.py b/genyrator/template_config.py index d4cc26a..4a00773 100644 --- a/genyrator/template_config.py +++ b/genyrator/template_config.py @@ -15,15 +15,15 @@ def create_template_config( - module_name: str, - db_import_path: str, - entities: List[Entity], - api_name: str, - api_description: str, + module_name: str, + db_import_statement: str, + entities: List[Entity], + api_name: str, + api_description: str, ) -> TemplateConfig: root_files = [ create_template( - Template.RootInit, ['__init__'], module_name=module_name, db_import_path=db_import_path, + Template.RootInit, ['__init__'], module_name=module_name, db_import_statement=db_import_statement, ), create_template(Template.Config, ['config'], module_name=module_name), ] @@ -37,11 +37,11 @@ def create_template_config( db_models = [ *[create_template( Template.SQLAlchemyModel, ['sqlalchemy', 'model', 'sqlalchemy_model'], - db_import_path=db_import_path, entity=e, module_name=module_name, + db_import_statement=db_import_statement, entity=e, module_name=module_name, out_path=Template.OutPath((['sqlalchemy', 'model'], e.class_name)) ) for e in entities], create_template( - Template.SQLAlchemyModelInit, ['sqlalchemy', 'model', '__init__'], db_import_path=db_import_path, + Template.SQLAlchemyModelInit, ['sqlalchemy', 'model', '__init__'], db_import_statement=db_import_statement, imports=[Template.Import(e.class_name, [e.class_name]) for e in entities], module_name=module_name, ), create_template(Template.ModelToDict, ['sqlalchemy', 'model_to_dict'], module_name=module_name), @@ -52,14 +52,14 @@ def create_template_config( create_template( Template.ConvertDictToMarshmallow, ['sqlalchemy', 'convert_dict_to_marshmallow_result'], - module_name=module_name, db_import_path=db_import_path, + module_name=module_name, db_import_statement=db_import_statement, ), ] fixtures = [ create_template(Template.Template, ['sqlalchemy', 'fixture', '__init__']), *[create_template( Template.Fixture, ['sqlalchemy', 'fixture', 'fixture'], module_name=module_name, - db_import_path=db_import_path, out_path=Template.OutPath((['sqlalchemy', 'fixture'], entity.class_name)), + db_import_statement=db_import_statement, out_path=Template.OutPath((['sqlalchemy', 'fixture'], entity.class_name)), entity=entity, ) for entity in entities] ] @@ -77,7 +77,7 @@ def create_template_config( *[create_template( Template.Resource, ['resources', 'resource'], entity=entity, out_path=Template.OutPath((['resources'], entity.class_name)), - db_import_path=db_import_path, module_name=module_name, + db_import_statement=db_import_statement, module_name=module_name, restplus_template=create_template( Template.RestplusModel, ['resources', 'restplus_model'], entity=entity ).render(), diff --git a/genyrator/templates/__init__.j2 b/genyrator/templates/__init__.j2 index 1cd5455..6f7b96b 100644 --- a/genyrator/templates/__init__.j2 +++ b/genyrator/templates/__init__.j2 @@ -1,7 +1,7 @@ from flask_marshmallow import Marshmallow from {{ template.module_name }}.resources import api from {{ template.module_name }}.config import config -from {{ template.db_import_path }} import db +{{ template.db_import_statement }} app = config() db.init_app(app) diff --git a/genyrator/templates/resources/resource.j2 b/genyrator/templates/resources/resource.j2 index e2e94d4..e87fa49 100644 --- a/genyrator/templates/resources/resource.j2 +++ b/genyrator/templates/resources/resource.j2 @@ -24,7 +24,7 @@ from sqlalchemy.orm import noload from {{ template.module_name }}.core.convert_dict import ( python_dict_to_json_dict, json_dict_to_python_dict ) -from {{ template.db_import_path }} import db +{{ template.db_import_statement }} {{ model_import }} from {{ template.module_name }}.sqlalchemy.convert_properties import ( convert_properties_to_sqlalchemy_properties, convert_sqlalchemy_properties_to_dict_properties diff --git a/genyrator/templates/sqlalchemy/convert_dict_to_marshmallow_result.j2 b/genyrator/templates/sqlalchemy/convert_dict_to_marshmallow_result.j2 index 3ffd492..814537f 100644 --- a/genyrator/templates/sqlalchemy/convert_dict_to_marshmallow_result.j2 +++ b/genyrator/templates/sqlalchemy/convert_dict_to_marshmallow_result.j2 @@ -4,7 +4,7 @@ from flask_marshmallow.sqla import ModelSchema from sqlalchemy.ext.declarative import DeclarativeMeta from sqlalchemy.orm import noload -from {{ template.db_import_path }} import db +{{ template.db_import_statement }} from {{ template.module_name }}.core.convert_dict import json_dict_to_python_dict from {{ template.module_name }}.domain.types import DomainModel, UserJson from {{ template.module_name }}.sqlalchemy.convert_properties import convert_properties_to_sqlalchemy_properties diff --git a/genyrator/templates/sqlalchemy/fixture/fixture.j2 b/genyrator/templates/sqlalchemy/fixture/fixture.j2 index 69acac1..6e0f778 100644 --- a/genyrator/templates/sqlalchemy/fixture/fixture.j2 +++ b/genyrator/templates/sqlalchemy/fixture/fixture.j2 @@ -1,7 +1,7 @@ {%- set entity = template.entity -%} import factory -from {{ template.db_import_path }} import db +{{ template.db_import_statement }} from {{ template.module_name }}.sqlalchemy.model.{{ entity.class_name }} import {{ entity.class_name }} diff --git a/genyrator/templates/sqlalchemy/model/sqlalchemy_model.j2 b/genyrator/templates/sqlalchemy/model/sqlalchemy_model.j2 index 74165c7..914bb98 100644 --- a/genyrator/templates/sqlalchemy/model/sqlalchemy_model.j2 +++ b/genyrator/templates/sqlalchemy/model/sqlalchemy_model.j2 @@ -2,7 +2,7 @@ from sqlalchemy_utils import UUIDType from sqlalchemy import UniqueConstraint from sqlalchemy.types import JSON as JSONType -from {{ template.db_import_path }} import db +{{ template.db_import_statement }} from {{ template.module_name }}.sqlalchemy.model.types import BigIntegerVariantType {%- macro pad(name) -%} diff --git a/test/spec/genyrator/entities/Schema_spec.py b/test/spec/genyrator/entities/Schema_spec.py new file mode 100644 index 0000000..3f317c9 --- /dev/null +++ b/test/spec/genyrator/entities/Schema_spec.py @@ -0,0 +1,16 @@ +from expects import expect, equal +from mamba import description, it + +from genyrator import create_schema +from genyrator.entities.Schema import DBImport + +with description('create_schema'): + with it('converts a DBImport to a import string'): + schema = create_schema( + module_name='module_name', + entities=[], + db_import=DBImport('db_module', 'db_variable'), + ) + + expect(schema.templates.root_files[0].db_import_statement)\ + .to(equal('from db_module import db_variable as db')) From 9801d180308e68908746bca19dc8996f19bef222 Mon Sep 17 00:00:00 2001 From: jumblesale Date: Tue, 5 Mar 2019 09:10:58 +0100 Subject: [PATCH 05/10] added note about f strings to TODO --- TODO.md | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/TODO.md b/TODO.md index 99480c6..0ec5a67 100644 --- a/TODO.md +++ b/TODO.md @@ -1,6 +1,7 @@ # 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 @@ -10,7 +11,7 @@ remaining tasks for the `0.2` release, in order of priority * [ ] pagination * [ ] hateoas-style support -## move behaviours +### 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. @@ -19,7 +20,7 @@ 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. -## validation +### validation 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 @@ -29,14 +30,14 @@ 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 +### declarative entity definitions 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 +### schema validation 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 @@ -45,20 +46,24 @@ own or something different? there could be the opportunity to allow the option to specify column-level validation - how would this look? -## callbacks +### callbacks 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 +### 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 +### pagination configurable pagination on a per-endpoint basis. -## hateoas-style support +### hateoas-style support location headers. explorable links. + +## minor + +* [ ] stop using `.format`, start using `f` strings everywhere From a92215493422d5549e4fb7256245021f5404f967 Mon Sep 17 00:00:00 2001 From: jumblesale Date: Tue, 5 Mar 2019 09:11:27 +0100 Subject: [PATCH 06/10] ... and eliminated a use of .format() --- genyrator/entities/File.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/genyrator/entities/File.py b/genyrator/entities/File.py index 7a4cb86..c5810ec 100644 --- a/genyrator/entities/File.py +++ b/genyrator/entities/File.py @@ -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, From ed036560d07463cddda0f31681e1208c1b1f210f Mon Sep 17 00:00:00 2001 From: jumblesale Date: Tue, 5 Mar 2019 09:57:57 +0100 Subject: [PATCH 07/10] added method for getting the db instance from a module --- genyrator/behaviour/__init__.py | 0 genyrator/behaviour/sqlalchemhy/__init__.py | 0 genyrator/behaviour/sqlalchemhy/db.py | 10 ++++++++++ test/spec/genyrator/behaviour/sqlalchemy/db_spec.py | 12 ++++++++++++ 4 files changed, 22 insertions(+) create mode 100644 genyrator/behaviour/__init__.py create mode 100644 genyrator/behaviour/sqlalchemhy/__init__.py create mode 100644 genyrator/behaviour/sqlalchemhy/db.py create mode 100644 test/spec/genyrator/behaviour/sqlalchemy/db_spec.py diff --git a/genyrator/behaviour/__init__.py b/genyrator/behaviour/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/genyrator/behaviour/sqlalchemhy/__init__.py b/genyrator/behaviour/sqlalchemhy/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/genyrator/behaviour/sqlalchemhy/db.py b/genyrator/behaviour/sqlalchemhy/db.py new file mode 100644 index 0000000..a137e47 --- /dev/null +++ b/genyrator/behaviour/sqlalchemhy/db.py @@ -0,0 +1,10 @@ +from importlib import import_module + + +def get_db_instance( + module_name: str, + variable_name: str, +): + module = import_module(module_name) + instance = getattr(module, variable_name) + return instance diff --git a/test/spec/genyrator/behaviour/sqlalchemy/db_spec.py b/test/spec/genyrator/behaviour/sqlalchemy/db_spec.py new file mode 100644 index 0000000..2bef426 --- /dev/null +++ b/test/spec/genyrator/behaviour/sqlalchemy/db_spec.py @@ -0,0 +1,12 @@ +from expects import expect, be_none +from mamba import description, it + +from genyrator.behaviour.sqlalchemhy.db import get_db_instance + +with description('accessing the db instance'): + with it('loads the db instance from a module'): + instance = get_db_instance( + module_name='bookshop.sqlalchemy', + variable_name='db', + ) + expect(instance).not_to(be_none) From cd5958620b287dda83a35782c746e685c8bb4adc Mon Sep 17 00:00:00 2001 From: jumblesale Date: Tue, 5 Mar 2019 16:22:01 +0100 Subject: [PATCH 08/10] refactored Schema to continue taking an import string * this means the changes are no longer bc-breaking * now you have to call `init_genyrator` before you can start using the app --- genyrator/behaviour/sqlalchemhy/db.py | 10 --------- .../{sqlalchemhy => sqlalchemy}/__init__.py | 0 genyrator/behaviour/sqlalchemy/db.py | 17 ++++++++++++++ genyrator/entities/Schema.py | 20 +++++++---------- .../genyrator/behaviour/sqlalchemy/db_spec.py | 22 +++++++++++-------- test/spec/genyrator/entities/Schema_spec.py | 3 +-- 6 files changed, 39 insertions(+), 33 deletions(-) delete mode 100644 genyrator/behaviour/sqlalchemhy/db.py rename genyrator/behaviour/{sqlalchemhy => sqlalchemy}/__init__.py (100%) create mode 100644 genyrator/behaviour/sqlalchemy/db.py diff --git a/genyrator/behaviour/sqlalchemhy/db.py b/genyrator/behaviour/sqlalchemhy/db.py deleted file mode 100644 index a137e47..0000000 --- a/genyrator/behaviour/sqlalchemhy/db.py +++ /dev/null @@ -1,10 +0,0 @@ -from importlib import import_module - - -def get_db_instance( - module_name: str, - variable_name: str, -): - module = import_module(module_name) - instance = getattr(module, variable_name) - return instance diff --git a/genyrator/behaviour/sqlalchemhy/__init__.py b/genyrator/behaviour/sqlalchemy/__init__.py similarity index 100% rename from genyrator/behaviour/sqlalchemhy/__init__.py rename to genyrator/behaviour/sqlalchemy/__init__.py diff --git a/genyrator/behaviour/sqlalchemy/db.py b/genyrator/behaviour/sqlalchemy/db.py new file mode 100644 index 0000000..ac45ce1 --- /dev/null +++ b/genyrator/behaviour/sqlalchemy/db.py @@ -0,0 +1,17 @@ +from functools import lru_cache + +from flask_sqlalchemy import SQLAlchemy + + +def init_genyrator( + sqlalchemy_instance: SQLAlchemy, +): + get_db_instance._db_instance = sqlalchemy_instance + + +@lru_cache() +def get_db_instance(): + if get_db_instance._db_instance is None: + ... + else: + return get_db_instance._db_instance diff --git a/genyrator/entities/Schema.py b/genyrator/entities/Schema.py index dc966b0..e29b3f3 100644 --- a/genyrator/entities/Schema.py +++ b/genyrator/entities/Schema.py @@ -1,15 +1,10 @@ -from typing import List, Optional, NamedTuple +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.template_config import create_template_config, TemplateConfig -DBImport = NamedTuple('DBImport', - [('db_module', str), - ('db_variable_name', str), ] -) - @attr.s class Schema(object): @@ -52,7 +47,7 @@ def write_domain_models(self): def create_schema( module_name: str, entities: List[Entity], - db_import: Optional[DBImport] = None, + db_import: Optional[str] = None, api_name: Optional[str] = None, api_description: Optional[str] = None, file_path: Optional[List[str]] = None, @@ -65,21 +60,22 @@ def create_schema( 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 DBImport describing how to access the sqlalchemy database object used by - the app. Defaults to importing "db" from "{module_name}.sqlalchemy" + 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 DBImport(f'{module_name}.sqlalchemy', 'db') - db_import_statement = f'from {db_import.db_module} import {db_import.db_variable_name} as db' + 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_statement=db_import_statement, + db_import_statement=db_import, entities=entities, api_name=api_name, api_description=api_description, diff --git a/test/spec/genyrator/behaviour/sqlalchemy/db_spec.py b/test/spec/genyrator/behaviour/sqlalchemy/db_spec.py index 2bef426..9a7a866 100644 --- a/test/spec/genyrator/behaviour/sqlalchemy/db_spec.py +++ b/test/spec/genyrator/behaviour/sqlalchemy/db_spec.py @@ -1,12 +1,16 @@ -from expects import expect, be_none +from expects import be_a, expect +from flask_sqlalchemy import SQLAlchemy from mamba import description, it -from genyrator.behaviour.sqlalchemhy.db import get_db_instance +from genyrator.behaviour.sqlalchemy.db import ( + init_genyrator, get_db_instance +) -with description('accessing the db instance'): - with it('loads the db instance from a module'): - instance = get_db_instance( - module_name='bookshop.sqlalchemy', - variable_name='db', - ) - expect(instance).not_to(be_none) +from bookshop.sqlalchemy import db as bookshop_db + + +with description('using the db instance'): + with it('provides access to the db instance'): + init_genyrator(sqlalchemy_instance=bookshop_db) + instance = get_db_instance() + expect(instance).to(be_a(SQLAlchemy)) diff --git a/test/spec/genyrator/entities/Schema_spec.py b/test/spec/genyrator/entities/Schema_spec.py index 3f317c9..8039934 100644 --- a/test/spec/genyrator/entities/Schema_spec.py +++ b/test/spec/genyrator/entities/Schema_spec.py @@ -2,14 +2,13 @@ from mamba import description, it from genyrator import create_schema -from genyrator.entities.Schema import DBImport with description('create_schema'): with it('converts a DBImport to a import string'): schema = create_schema( module_name='module_name', entities=[], - db_import=DBImport('db_module', 'db_variable'), + db_import='from db_module import db_variable as db', ) expect(schema.templates.root_files[0].db_import_statement)\ From d9014b78af54c1628140d265d624828ab397fd7d Mon Sep 17 00:00:00 2001 From: jumblesale Date: Wed, 6 Mar 2019 09:42:08 +0100 Subject: [PATCH 09/10] added test for trying to use methods before db has been initialized --- genyrator/behaviour/sqlalchemy/db.py | 11 ++++++++--- .../genyrator/behaviour/sqlalchemy/db_spec.py | 15 ++++++++++++--- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/genyrator/behaviour/sqlalchemy/db.py b/genyrator/behaviour/sqlalchemy/db.py index ac45ce1..7560562 100644 --- a/genyrator/behaviour/sqlalchemy/db.py +++ b/genyrator/behaviour/sqlalchemy/db.py @@ -3,15 +3,20 @@ 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 -@lru_cache() def get_db_instance(): - if get_db_instance._db_instance is None: - ... + if not hasattr(get_db_instance, '_db_instance'): + raise DBNotInitialisedException(DBNotInitialisedException.message) else: return get_db_instance._db_instance diff --git a/test/spec/genyrator/behaviour/sqlalchemy/db_spec.py b/test/spec/genyrator/behaviour/sqlalchemy/db_spec.py index 9a7a866..922eeb3 100644 --- a/test/spec/genyrator/behaviour/sqlalchemy/db_spec.py +++ b/test/spec/genyrator/behaviour/sqlalchemy/db_spec.py @@ -1,15 +1,24 @@ -from expects import be_a, expect +from expects import be_a, expect, be_none from flask_sqlalchemy import SQLAlchemy from mamba import description, it from genyrator.behaviour.sqlalchemy.db import ( - init_genyrator, get_db_instance -) + init_genyrator, get_db_instance, + DBNotInitialisedException) from bookshop.sqlalchemy import db as bookshop_db with description('using the db instance'): + with it('throws if the db has not been initialized'): + exception = None + try: + instance = get_db_instance() + except DBNotInitialisedException as e: + exception = e + expect(exception).to_not(be_none) + + with it('provides access to the db instance'): init_genyrator(sqlalchemy_instance=bookshop_db) instance = get_db_instance() From 1442964c491f08b40c3be33bbbae37bf9b72019a Mon Sep 17 00:00:00 2001 From: jumblesale Date: Wed, 6 Mar 2019 15:30:00 +0100 Subject: [PATCH 10/10] bumped requests version in requirements.txt due to security warning --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 86f19fa..6ad73cb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -29,7 +29,7 @@ pyhamcrest==1.9.0 python-dateutil==2.7.3 pytz==2018.5 requests-toolbelt==0.8.0 -requests==2.19.1 +requests==2.20.0 six==1.11.0 sqlalchemy-utils==0.33.3 sqlalchemy==1.2.10