diff --git a/README.md b/README.md index 80151998..1f377fb0 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ automatically [pip](https://pip.pypa.io/en/stable/) as well. - Install the [Microsoft ODBC Driver for SQL Server](https://docs.microsoft.com/en-us/sql/connect/odbc/microsoft-odbc-driver-for-sql-server?view=sql-server-ver15) -in your operative system. Then you have to check out how is called the SQL Driver installation. +in your operative system. Then you have to check out what is the name of the SQL Driver installation. Check it out with: ```bash @@ -55,7 +55,7 @@ Driver=/usr/local/lib/libmsodbcsql.17.dylib UsageCount=2 ``` -Then when you specify the driver name, e.g. _DBC Driver 17 for SQL Server_ in the `DATABASE_URI`. E.g. +Then specify the driver name, in this case _DBC Driver 17 for SQL Server_ in the `DATABASE_URI`, e.g.: ```.dotenv DATABASE_URI=mssql+pyodbc://:@time-tracker-srv.database.windows.net/?driver\=ODBC Driver 17 for SQL Server diff --git a/cli.py b/cli.py index 953e0e06..10fc8818 100644 --- a/cli.py +++ b/cli.py @@ -6,9 +6,11 @@ from flask_script import Manager from time_tracker_api import create_app -from time_tracker_api.api import api app = create_app() + +from time_tracker_api.api import api + cli_manager = Manager(app) @@ -44,6 +46,23 @@ def gen_postman_collection(filename='timetracker-api-postman-collection.json', save_data(parsed_json, filename) +@cli_manager.command +def seed(): + from time_tracker_api.database import seeder as seed + seed() + + +@cli_manager.command +def re_create_db(): + print('This command will drop all tables and seed again the database') + confirm_answer = input('Do you confirm (Y) you want to remove all your data?\n') + if confirm_answer.upper() == 'Y': + from time_tracker_api.database import seeder + seeder.fresh() + else: + print('\nThis command was cancelled!') + + def save_data(data: str, filename: str) -> None: """ Save text content to a file """ if filename: diff --git a/requirements/dev.txt b/requirements/dev.txt index ce9474c3..e1d4d47d 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -12,7 +12,4 @@ pytest-mock==2.0.0 Faker==4.0.2 # Coverage -coverage==4.5.1 - -# SQL database (MS SQL) -flask_sqlalchemy==2.4.1 +coverage==4.5.1 \ No newline at end of file diff --git a/requirements/prod.txt b/requirements/prod.txt index df3d5cfc..a88e3bd2 100644 --- a/requirements/prod.txt +++ b/requirements/prod.txt @@ -13,7 +13,13 @@ Jinja2==2.11.1 gunicorn==20.0.4 #Swagger support for Restful API -flask-restplus==0.13.0 +flask-restplus==0.12.1 #CLI support -Flask-Script==2.0.6 \ No newline at end of file +Flask-Script==2.0.6 + +# SQL database (MS SQL) +flask_sqlalchemy==2.4.1 + +# Handling requests +requests==2.23.0 \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index 05e9f880..3d7db568 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -22,16 +22,15 @@ def client(app: Flask) -> FlaskClient: @pytest.fixture(scope="module") -def sql_repository(app: Flask): - with app.app_context(): - from .resources import TestModel - from time_tracker_api.sql_repository import db +def sql_repository(): + from .resources import PersonSQLModel + from time_tracker_api.database import seeder + from time_tracker_api.sql_repository import db - db.create_all() - print("Models for test created!") + seeder.fresh() - from time_tracker_api.sql_repository import SQLRepository - yield SQLRepository(TestModel) + from time_tracker_api.sql_repository import SQLRepository + yield SQLRepository(PersonSQLModel) - print("Models for test removed!") - db.drop_all() + db.drop_all() + print("Models for test removed!") diff --git a/tests/projects/projects_namespace_test.py b/tests/projects/projects_namespace_test.py index 11154e56..e05f2285 100644 --- a/tests/projects/projects_namespace_test.py +++ b/tests/projects/projects_namespace_test.py @@ -1,11 +1,17 @@ from flask import json +from flask.testing import FlaskClient +from pytest_mock import MockFixture -def test_list_should_return_nothing(client): - """Should return an empty array""" +def test_list_all_elements(client: FlaskClient, mocker: MockFixture): + """Should return all elements in a list""" + from time_tracker_api.projects.projects_namespace import project_dao + repository_find_all_mock = mocker.patch.object(project_dao.repository, 'find_all', return_value=[]) + response = client.get("/projects", follow_redirects=True) assert 200 == response.status_code json_data = json.loads(response.data) assert [] == json_data + repository_find_all_mock.assert_called_once() diff --git a/tests/resources.py b/tests/resources.py index 6175e0eb..8746fe14 100644 --- a/tests/resources.py +++ b/tests/resources.py @@ -1,9 +1,11 @@ -from time_tracker_api.sql_repository import db +from time_tracker_api.database import AuditedModel +from time_tracker_api.sql_repository import db, SQLAuditedModel -class TestModel(db.Model): +class PersonSQLModel(db.Model, SQLAuditedModel): + __tablename__ = 'tests' id = db.Column(db.Integer, primary_key=True) - name = db.Column(db.String(80), unique=True, nullable=False) + name = db.Column(db.String(80), unique=False, nullable=False) email = db.Column(db.String(120), unique=True, nullable=False) age = db.Column(db.Integer, nullable=False) diff --git a/tests/smoke_test.py b/tests/smoke_test.py index 3e0551d6..90f6cecd 100644 --- a/tests/smoke_test.py +++ b/tests/smoke_test.py @@ -1,4 +1,3 @@ - def test_app_exists(app): """Does app exists""" assert app is not None diff --git a/tests/sql_repository_test.py b/tests/sql_repository_test.py index 896502f4..866f9673 100644 --- a/tests/sql_repository_test.py +++ b/tests/sql_repository_test.py @@ -8,16 +8,19 @@ def test_create(sql_repository): """Should create a new Entry""" - from .resources import TestModel global sample_element - sample_element = TestModel(name=fake.name(), - email=fake.safe_email(), - age=fake.pyint(min_value=10, max_value=80)) + sample_element = dict(name=fake.name(), + email=fake.safe_email(), + age=fake.pyint(min_value=10, max_value=80)) result = sql_repository.create(sample_element) assert result is not None assert result.id is not None + assert result.created_at is not None + assert result.created_by is not None + assert result.updated_at is None + assert result.updated_by is None existing_elements_registry.append(result) @@ -43,6 +46,9 @@ def test_update(sql_repository): assert updated_element.id == existing_element.id assert updated_element.name == "Jon Snow" assert updated_element.age == 34 + assert updated_element.updated_at is not None + assert updated_element.updated_at > updated_element.created_at + assert updated_element.updated_by is not None def test_find_all(sql_repository): @@ -54,10 +60,10 @@ def test_find_all(sql_repository): def test_find_all_that_contains_property_with_string(sql_repository): """Find all elements that have a property that partially contains a string (case-insensitive)""" - from .resources import TestModel - new_element = TestModel(name='Ramsay Snow', - email=fake.safe_email(), - age=fake.pyint(min_value=10, max_value=80)) + fake_name = fake.name() + new_element = dict(name="%s Snow" % fake_name, + email=fake.safe_email(), + age=fake.pyint(min_value=10, max_value=80)) sql_repository.create(new_element) existing_elements_registry.append(new_element) @@ -67,8 +73,8 @@ def test_find_all_that_contains_property_with_string(sql_repository): search_jon_result = sql_repository.find_all_contain_str('name', 'Jon') assert len(search_jon_result) == 1 - search_ram_result = sql_repository.find_all_contain_str('name', 'RAM') - assert search_ram_result[0] is new_element + search_ram_result = sql_repository.find_all_contain_str('name', fake_name) + assert search_ram_result[0].name == new_element['name'] def test_delete_existing_element(sql_repository): diff --git a/tests/time_entries/__init__.py b/tests/time_entries/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/time_entries/time_entries_namespace_test.py b/tests/time_entries/time_entries_namespace_test.py deleted file mode 100644 index 1111309e..00000000 --- a/tests/time_entries/time_entries_namespace_test.py +++ /dev/null @@ -1,17 +0,0 @@ -from flask import json -from flask.testing import FlaskClient -from pytest_mock import MockFixture - - -def test_list_should_return_empty_array(mocker: MockFixture, client: FlaskClient): - from time_tracker_api.time_entries.time_entries_namespace import model - """Should return an empty array""" - model_mock = mocker.patch.object(model, 'find_all', return_value=[]) - - response = client.get("/time-entries", follow_redirects=True) - - assert 200 == response.status_code - - json_data = json.loads(response.data) - assert [] == json_data - model_mock.assert_called_once() diff --git a/time_tracker_api/__init__.py b/time_tracker_api/__init__.py index a37e669c..18d1755a 100644 --- a/time_tracker_api/__init__.py +++ b/time_tracker_api/__init__.py @@ -2,9 +2,12 @@ from flask import Flask +flask_app = None + def create_app(config_path='time_tracker_api.config.DefaultConfig', config_data=None): + global flask_app flask_app = Flask(__name__) init_app_config(flask_app, config_path, config_data) diff --git a/time_tracker_api/activities/activities_namespace.py b/time_tracker_api/activities/activities_namespace.py index 0bd3504f..3983dad6 100644 --- a/time_tracker_api/activities/activities_namespace.py +++ b/time_tracker_api/activities/activities_namespace.py @@ -1,6 +1,7 @@ +from faker import Faker from flask_restplus import fields, Resource, Namespace + from time_tracker_api.api import audit_fields -from faker import Faker faker = Faker() diff --git a/time_tracker_api/api.py b/time_tracker_api/api.py index b34d5618..9e62f72b 100644 --- a/time_tracker_api/api.py +++ b/time_tracker_api/api.py @@ -1,9 +1,15 @@ -from flask_restplus import Api, fields +import pyodbc + +import sqlalchemy from faker import Faker +from flask import current_app as app +from flask_restplus import Api, fields +from requests.exceptions import ConnectionError faker = Faker() -api = Api(version='1.0.1', title="TimeTracker API", +api = Api(version='1.0.1', + title="TimeTracker API", description="API for the TimeTracker project") # Common models structure @@ -14,31 +20,73 @@ description='Date of creation', example=faker.iso8601(end_datetime=None), ), - 'tenant_id': fields.String( + 'updated_at': fields.Date( readOnly=True, - title='Tenant', - max_length=64, - description='The tenant this belongs to', - example=faker.random_int(1, 9999), + title='Updated', + description='Date of update', + example=faker.iso8601(end_datetime=None), ), + # TODO Activate it when the tenants model is implemented + # 'tenant_id': fields.String( + # readOnly=True, + # title='Tenant', + # max_length=64, + # description='The tenant this belongs to', + # example=faker.random_int(1, 9999), + # ), 'created_by': fields.String( readOnly=True, title='Creator', max_length=64, description='User that created it', - example=faker.random_int(1, 9999), + example='anonymous', + ), + 'updated_by': fields.String( + readOnly=True, + title='Updater', + max_length=64, + description='User that updated it', + example='anonymous', ), } # APIs from time_tracker_api.projects import projects_namespace + api.add_namespace(projects_namespace.ns) from time_tracker_api.activities import activities_namespace + api.add_namespace(activities_namespace.ns) from time_tracker_api.technologies import technologies_namespace + api.add_namespace(technologies_namespace.ns) from time_tracker_api.time_entries import time_entries_namespace + api.add_namespace(time_entries_namespace.ns) + +""" +Error handlers +""" + + +@api.errorhandler(pyodbc.IntegrityError) +@api.errorhandler(sqlalchemy.exc.IntegrityError) +def handle_db_integrity_error(e): + """Return a 400 status code error""" + return {'message': "This element already exists"}, 400 + + +@api.errorhandler(pyodbc.OperationalError) +def handle_connection_error(e): + """Return a 500 due to a issue in the connection to a 3rd party service""" + return {'message': 'Connection issues. Try in a few minutes.'}, 500 + + +@api.errorhandler +def generic_exception_handler(e): + if not app.config.get("FLASK_DEBUG", False): + app.logger.error(e) + return {'message': 'An unhandled exception occurred.'}, 500 diff --git a/time_tracker_api/config.py b/time_tracker_api/config.py index c7a8f62d..9523b8c6 100644 --- a/time_tracker_api/config.py +++ b/time_tracker_api/config.py @@ -1,10 +1,11 @@ import os +from time_tracker_api.database import DATABASE + class Config: DEBUG = False - class DevelopConfig(Config): DEBUG = True FLASK_DEBUG = True @@ -13,7 +14,7 @@ class DevelopConfig(Config): class AzureSQLDatabaseDevelopConfig(DevelopConfig): - DATABASE = 'sql' + DATABASE = DATABASE.SQL TEST_TABLE = 'tests' SQLALCHEMY_COMMIT_ON_TEARDOWN = True SQLALCHEMY_TRACK_MODIFICATIONS = False diff --git a/time_tracker_api/database.py b/time_tracker_api/database.py index c4cc4a95..6e362ec1 100644 --- a/time_tracker_api/database.py +++ b/time_tracker_api/database.py @@ -2,28 +2,110 @@ Agnostic database assets Put here your utils and class independent of -the database solution +the database solution. +To know more about protocols and subtyping check out PEP-0544 """ +import abc +import enum +from datetime import datetime + from flask import Flask -RepositoryModel = None + +class DATABASE(enum.Enum): + IN_MEMORY = 'in-memory' + SQL = 'sql' + + +class CRUDDao(abc.ABC): + @abc.abstractmethod + def get_all(self): + pass + + @abc.abstractmethod + def get(self, id): + pass + + @abc.abstractmethod + def create(self, project): + pass + + @abc.abstractmethod + def update(self, id, data): + pass + + @abc.abstractmethod + def delete(self, id): + pass + + +class Seeder(abc.ABC): + @abc.abstractmethod + def run(self): + """Provision database""" + pass + + @abc.abstractmethod + def fresh(self): + """will drop all tables and seed again the database""" + pass + + def __call__(self, *args, **kwargs): + self.run() + + +class DatabaseModel: + """ + Represents a model of a particular database, + e.g. SQL Model + """ + + def to_dto(self): + return self + + +def convert_result_to_dto(f): + def convert_if_necessary(result): + if hasattr(result, 'to_dto'): + return result.to_dto() + elif issubclass(type(result), list): + return list(map(convert_if_necessary, result)) + return result + + def to_dto(*args, **kw): + """ + Decorator that converts any result that is a + DatabaseModel into its correspondent dto. + """ + result = f(*args, **kw) + return convert_if_necessary(result) + return to_dto + + +class AuditedModel(abc.ABC): + def __init__(self, + created_at: datetime, + updated_at: datetime, + created_by: str, + updated_by: str): + self.updated_by = updated_by + self.created_by = created_by + self.updated_at = updated_at + self.created_at = created_at + + +seeder: Seeder = None def init_app(app: Flask) -> None: """Make the app ready to use the database""" - database_strategy_name = app.config['DATABASE'] + database_strategy = app.config['DATABASE'] with app.app_context(): - module = globals()["use_%s" % database_strategy_name](app) - global RepositoryModel - RepositoryModel = module.repository_model - - -def create(model_name: str): - """Creates the repository instance for the chosen database""" - return RepositoryModel(model_name) + globals()["use_%s" % database_strategy.name.lower()](app) def use_sql(app: Flask) -> None: - from time_tracker_api import sql_repository - sql_repository.init_app(app) - return sql_repository + from time_tracker_api.sql_repository import init_app, SQLSeeder + init_app(app) + global seeder + seeder = SQLSeeder() diff --git a/time_tracker_api/projects/projects_model.py b/time_tracker_api/projects/projects_model.py index 7fc286e1..30caf976 100644 --- a/time_tracker_api/projects/projects_model.py +++ b/time_tracker_api/projects/projects_model.py @@ -1,80 +1,82 @@ -from time_tracker_api.errors \ - import MissingResource, InvalidInput, InvalidMatch +import enum +from flask import Flask -class InMemoryProjectDAO(object): - def __init__(self): - self.counter = 0 - self.projects = [] +from time_tracker_api.database import DATABASE, CRUDDao +from time_tracker_api.sql_repository import SQLCRUDDao, SQLAuditedModel, SQLModel - def get_all(self): - return self.projects - def get(self, id): - for project in self.projects: - if project.get('id') == id: +class PROJECT_TYPE(enum.Enum): + CUSTOMER = 'CUSTOMER' + TRAINING = 'TRAINING' + + @classmethod + def valid_type_values(self): + return list(map(lambda x: x.value, PROJECT_TYPE._member_map_.values())) + + +class ProjectDao(CRUDDao): + pass + + +def create_dao(app: Flask) -> ProjectDao: + """This will construct the dao based on the chosen DATABASE""" + if app.config['DATABASE'] == DATABASE.SQL: + from time_tracker_api.sql_repository import db + + class ProjectSQLModel(db.Model, SQLModel, SQLAuditedModel): + __tablename__ = 'projects' + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(50), unique=True, nullable=False) + description = db.Column(db.String(250), unique=True, nullable=False) + type = db.Column(db.String(10), nullable=False) + active = db.Column(db.Boolean, default=True) + + def __repr__(self): + return '' % self.name + + def __str___(self): + return "the project \"%s\"" % self.name + + class ProjectSQLDao(SQLCRUDDao): + def __init__(self): + SQLCRUDDao.__init__(self, ProjectSQLModel) + + return ProjectSQLDao() + else: + from time_tracker_api.errors import MissingResource + + class ProjectInMemoryDAO(object): + def __init__(self): + self.counter = 0 + self.projects = [] + + def get_all(self): + return self.projects + + def get(self, id): + for project in self.projects: + if project.get('id') == id: + return project + raise MissingResource("Project '%s' not found" % id) + + def create(self, project): + self.counter += 1 + project['id'] = str(self.counter) + self.projects.append(project) return project - raise MissingResource("Project '%s' not found" % id) - - def create(self, project): - self.counter += 1 - project['id'] = str(self.counter) - self.projects.append(project) - return project - - def update(self, id, data): - project = self.get(id) - if project: - project.update(data) - return project - else: - raise MissingResource("Project '%s' not found" % id) - - def delete(self, id): - if id: - project = self.get(id) - self.projects.remove(project) - - def flush(self): - self.projects.clear() - - def search(self, search_criteria): - matching_projects = self.select_matching_projects(search_criteria) - - if len(matching_projects) > 0: - return matching_projects - else: - raise InvalidMatch("No project matched the specified criteria") - - def select_matching_projects(self, user_search_criteria): - search_criteria = {k: v for k, v - in user_search_criteria.items() - if v is not None} - - def matches_search_string(search_str, project): - return search_str in project['comments'] or \ - search_str in project['short_name'] - - if not search_criteria: - raise InvalidInput("No search criteria specified") - - search_str = search_criteria.get('search_string') - if search_str: - matching_projects = [p for p - in self.projects - if matches_search_string(search_str, p)] - else: - matching_projects = self.projects - - is_active = search_criteria.get('active') - if is_active is not None: - matching_projects = [p for p - in matching_projects - if p['active'] is is_active] - - return matching_projects - - -# Instances -# TODO Create an strategy to create other types of DAO -project_dao = InMemoryProjectDAO() + + def update(self, id, data): + project = self.get(id) + if project: + project.update(data) + return project + else: + raise MissingResource("Project '%s' not found" % id) + + def delete(self, id): + if id: + project = self.get(id) + self.projects.remove(project) + + return ProjectInMemoryDAO() diff --git a/time_tracker_api/projects/projects_namespace.py b/time_tracker_api/projects/projects_namespace.py index fb616934..d2f76491 100644 --- a/time_tracker_api/projects/projects_namespace.py +++ b/time_tracker_api/projects/projects_namespace.py @@ -1,8 +1,10 @@ -from flask_restplus import Namespace, Resource, abort, inputs, fields -from .projects_model import project_dao -from time_tracker_api.errors import MissingResource -from time_tracker_api.api import audit_fields from faker import Faker +from flask_restplus import Namespace, Resource, abort, fields + +from time_tracker_api import flask_app +from time_tracker_api.api import audit_fields +from time_tracker_api.errors import MissingResource +from .projects_model import PROJECT_TYPE, create_dao faker = Faker() @@ -19,15 +21,23 @@ ), 'description': fields.String( title='Description', + max_length=250, description='Description about the project', example=faker.paragraph(), ), 'type': fields.String( - required=True, + required=False, title='Type', - max_length=30, + max_length=10, description='If it is `Costumer`, `Training` or other type', - example=faker.word(['Customer', 'Training']), + enum=PROJECT_TYPE.valid_type_values(), + example=faker.word(PROJECT_TYPE.valid_type_values()), + ), + 'active': fields.Boolean( + title='Is active?', + description='Whether the project is active or not', + default=True, + example=faker.boolean(), ), }) @@ -48,6 +58,8 @@ project_response_fields ) +project_dao = create_dao(flask_app) + @ns.route('') class Projects(Resource): @@ -64,13 +76,9 @@ def post(self): """Create a project""" return project_dao.create(ns.payload), 201 + # TODO : fix, this parser is for a field that is not being used. project_update_parser = ns.parser() -project_update_parser.add_argument('active', - type=inputs.boolean, - location='form', - required=True, - help='Is the project active?') @ns.route('/') @@ -96,13 +104,6 @@ def post(self, id): except MissingResource as e: abort(message=str(e), code=404) - @ns.doc('put_project') - @ns.expect(project_input) - @ns.marshal_with(project) - def put(self, id): - """Create or replace a project""" - return project_dao.update(id, ns.payload) - @ns.doc('delete_project') @ns.response(204, 'Project deleted successfully') def delete(self, id): diff --git a/time_tracker_api/security.py b/time_tracker_api/security.py new file mode 100644 index 00000000..d2ded5b8 --- /dev/null +++ b/time_tracker_api/security.py @@ -0,0 +1,12 @@ +""" +This is where we handle everything regarding to authorization +and authentication. Also stores helper functions related to it. +""" + + +def current_user_id(): + """ + Returns the id of the authenticated user in + Azure Active Directory + """ + return 'anonymous' diff --git a/time_tracker_api/sql_repository.py b/time_tracker_api/sql_repository.py index d130535e..886f5040 100644 --- a/time_tracker_api/sql_repository.py +++ b/time_tracker_api/sql_repository.py @@ -1,7 +1,16 @@ +import json +from datetime import datetime + from flask import Flask from flask_sqlalchemy import SQLAlchemy +from sqlalchemy.ext.declarative import DeclarativeMeta + +from time_tracker_api.database import CRUDDao, Seeder, DatabaseModel, convert_result_to_dto +from time_tracker_api.security import current_user_id db = None +SQLModel = None +SQLAuditedModel = None def init_app(app: Flask) -> None: @@ -9,25 +18,45 @@ def init_app(app: Flask) -> None: global db db = SQLAlchemy(app) + global SQLModel + + class SQLModelClass(DatabaseModel): + def to_dto(self): + return self + + SQLModel = SQLModelClass + + global SQLAuditedModel + + class SQLAuditedModelClass(): + created_at = db.Column(db.DateTime, server_default=db.func.now()) + updated_at = db.Column(db.DateTime, onupdate=datetime.utcnow) + created_by = db.Column(db.String, default=current_user_id) + updated_by = db.Column(db.String, onupdate=current_user_id) + + SQLAuditedModel = SQLAuditedModelClass + class SQLRepository(): def __init__(self, model_type: type): self.model_type = model_type - def create(self, element: dict) -> dict: + def create(self, data: dict): + element = self.model_type(**data) db.session.add(element) db.session.commit() return element - def find(self, id: int) -> dict: + def find(self, id: int): return self.model_type.query.filter_by(id=id).first_or_404() - def find_all(self) -> list: + def find_all(self): return self.model_type.query.all() - def update(self, id: int, new_data: dict) -> dict: + def update(self, id: int, new_data): model = self.model_type.query.filter_by(id=id) model.update(new_data) + db.session.commit() return model.first_or_404() def remove(self, id: int) -> None: @@ -35,10 +64,44 @@ def remove(self, id: int) -> None: db.session.delete(found_element) db.session.commit() - def find_all_contain_str(self, property, text) -> list: - return self.model_type.query\ - .filter(getattr(self.model_type, property).contains(text))\ + def find_all_contain_str(self, property, text): + return self.model_type.query \ + .filter(getattr(self.model_type, property).contains(text)) \ .all() -repository_model = SQLRepository +class SQLCRUDDao(CRUDDao): + def __init__(self, model): + self.repository = SQLRepository(model) + + @convert_result_to_dto + def get_all(self): + return self.repository.find_all() + + @convert_result_to_dto + def get(self, id): + return self.repository.find(id) + + @convert_result_to_dto + def create(self, element): + return self.repository.create(element) + + @convert_result_to_dto + def update(self, id, data): + return self.repository.update(id, data) + + def delete(self, id): + self.repository.remove(id) + + +class SQLSeeder(Seeder): + def run(self): + print("Provisioning database...") + db.create_all() + print("Database seeded!") + + def fresh(self): + print("Removing all existing data...") + db.drop_all() + + self.run() diff --git a/time_tracker_api/technologies/technologies_namespace.py b/time_tracker_api/technologies/technologies_namespace.py index f9b72c11..2f3bb9fd 100644 --- a/time_tracker_api/technologies/technologies_namespace.py +++ b/time_tracker_api/technologies/technologies_namespace.py @@ -1,6 +1,7 @@ +from faker import Faker from flask_restplus import Namespace, Resource, fields + from time_tracker_api.api import audit_fields -from faker import Faker faker = Faker() diff --git a/time_tracker_api/time_entries/time_entries_model.py b/time_tracker_api/time_entries/time_entries_model.py new file mode 100644 index 00000000..816490ce --- /dev/null +++ b/time_tracker_api/time_entries/time_entries_model.py @@ -0,0 +1,32 @@ +import abc + +from flask import Flask + +from time_tracker_api.database import CRUDDao + + +class TimeEntriesDao(CRUDDao): + @abc.abstractmethod + def get_all(self): + pass + + @abc.abstractmethod + def get(self, id): + pass + + @abc.abstractmethod + def create(self, project): + pass + + @abc.abstractmethod + def update(self, id, data): + pass + + @abc.abstractmethod + def delete(self, id): + pass + + +def create_dao(app: Flask) -> TimeEntriesDao: + # TODO Create implementation(s) + return TimeEntriesDao() diff --git a/time_tracker_api/time_entries/time_entries_namespace.py b/time_tracker_api/time_entries/time_entries_namespace.py index f61f0a1b..f3238967 100644 --- a/time_tracker_api/time_entries/time_entries_namespace.py +++ b/time_tracker_api/time_entries/time_entries_namespace.py @@ -1,7 +1,7 @@ +from faker import Faker from flask_restplus import fields, Resource, Namespace + from time_tracker_api.api import audit_fields -from time_tracker_api import database -from faker import Faker faker = Faker() @@ -72,16 +72,13 @@ ) -model = database.create('time-entries') - - @ns.route('') class TimeEntries(Resource): @ns.doc('list_time_entries') @ns.marshal_list_with(time_entry, code=200) def get(self): """List all available time entries""" - return model.find_all() + return [], 200 @ns.doc('create_time_entry') @ns.expect(time_entry_input)