Skip to content
Merged
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ We are using Pytest](https://docs.pytest.org/en/latest/index.html) for tests. Th

To run the tests just execute:

```
```bash
python3 -m pytest -v
```

Expand Down
5 changes: 4 additions & 1 deletion requirements/dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
# For development

# Tests
pytest==4.1.1
pytest==5.2.0

# Mocking
pytest-mock==2.0.0

# Coverage
coverage==4.5.1
11 changes: 7 additions & 4 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@

import pytest
from flask import Flask
from flask.testing import FlaskClient

from time_tracker_api import create_app


@pytest.fixture(scope='session')
def app():
def app() -> Flask:
"""An instance of the app for tests"""
return create_app()


@pytest.fixture
def client(app):
def client(app: Flask) -> FlaskClient:
"""A test client for the app."""
with app.test_client() as c:
return c
return c
2 changes: 1 addition & 1 deletion tests/projects/projects_namespace_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ def test_list_should_return_nothing(client):
assert 200 == response.status_code

json_data = json.loads(response.data)
assert [] == json_data
assert [] == json_data
Empty file added tests/time_entries/__init__.py
Empty file.
17 changes: 17 additions & 0 deletions tests/time_entries/time_enties_namespace_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
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()
26 changes: 25 additions & 1 deletion time_tracker_api/__init__.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,38 @@
import os

from flask import Flask


def create_app():
def create_app(config_path='time_tracker_api.config.DefaultConfig',
config_data=None):
flask_app = Flask(__name__)

init_app_config(flask_app, config_path, config_data)
init_app(flask_app)

return flask_app


def init_app_config(app: Flask, config_path: str, config_data: dict = None):
if config_path:
app.config.from_object(config_path)
else:
# ensure the instance folder exists
try:
os.makedirs(app.instance_path)
except OSError:
pass

# Located in `/instance`
app.config.from_pyfile('config.py', silent=True)

if config_data:
app.config.update(config_data)


def init_app(app: Flask):
from .database import init_app as init_database
init_database(app)

from .api import api
api.init_app(app)
20 changes: 10 additions & 10 deletions time_tracker_api/activities/activities_namespace.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
ns = Namespace('activities', description='API for activities')

# Activity Model
activity = ns.model('Activity', {
activity_input = ns.model('ActivityInput', {
'name': fields.String(
required=True,
title='Name',
Expand All @@ -27,24 +27,24 @@
}
activity_response_fields.update(audit_fields)

activity_response = ns.inherit(
'ActivityResponse',
activity,
activity = ns.inherit(
'Activity',
activity_input,
activity_response_fields
)


@ns.route('')
class Activities(Resource):
@ns.doc('list_activities')
@ns.marshal_list_with(activity_response, code=200)
@ns.marshal_list_with(activity, code=200)
def get(self):
"""List all available activities"""
return []

@ns.doc('create_activity')
@ns.expect(activity)
@ns.marshal_with(activity_response, code=201)
@ns.expect(activity_input)
@ns.marshal_with(activity, code=201)
@ns.response(400, 'Invalid format of the attributes of the activity.')
def post(self):
"""Create a single activity"""
Expand All @@ -56,7 +56,7 @@ def post(self):
@ns.param('id', 'The unique identifier of the activity')
class Activity(Resource):
@ns.doc('get_activity')
@ns.marshal_with(activity_response)
@ns.marshal_with(activity)
def get(self, id):
"""Retrieve all the data of a single activity"""
return {}
Expand All @@ -69,8 +69,8 @@ def delete(self, id):

@ns.doc('put_activity')
@ns.response(400, 'Invalid format of the attributes of the activity.')
@ns.expect(activity)
@ns.marshal_with(activity_response)
@ns.expect(activity_input)
@ns.marshal_with(activity)
def put(self, id):
"""Updates an activity"""
return ns.payload
12 changes: 12 additions & 0 deletions time_tracker_api/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
class Config:
DEBUG = False


class WhateverDevelopConfig(Config):
DEBUG = True
FLASK_DEBUG = True
FLASK_ENV = "develop"
DATABASE = "whatever"


DefaultConfig = WhateverDevelopConfig
28 changes: 28 additions & 0 deletions time_tracker_api/database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""
Agnostic database assets

Put here your utils and class independent of
the database solution
"""
from flask import Flask

RepositoryModel = None


def init_app(app: Flask) -> None:
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not call this function 'init_database' directly ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There is some sort of standard that some objects like the APIs in flask Restplus have a function called init_app so they take the Flask app as a parameter and they do whatever initialization logic they need to be ready to be used by that app. So whatever module or package you add if you want to make it like pluggable to Flask this is pretty much the way to go

"""Make the app ready to use the database"""
database_strategy_name = app.config['DATABASE']
with app.app_context():
module = globals()["use_%s" % database_strategy_name]()
global RepositoryModel
RepositoryModel = module.repository_model


def create(model_name: str):
"""Creates the repository instance for the chosen database"""
return RepositoryModel(model_name)


def use_whatever():
from time_tracker_api import whatever_repository
return whatever_repository
20 changes: 10 additions & 10 deletions time_tracker_api/projects/projects_namespace.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
ns = Namespace('projects', description='API for projects (clients)')

# Project Model
project = ns.model('Project', {
project_input = ns.model('ProjectInput', {
'name': fields.String(
required=True,
title='Name',
Expand Down Expand Up @@ -35,24 +35,24 @@
}
project_response_fields.update(audit_fields)

project_response = ns.inherit(
'ProjectResponse',
project,
project = ns.inherit(
'Project',
project_input,
project_response_fields
)


@ns.route('')
class Projects(Resource):
@ns.doc('list_projects')
@ns.marshal_list_with(project_response, code=200)
@ns.marshal_list_with(project, code=200)
def get(self):
"""List all projects"""
return project_dao.get_all(), 200

@ns.doc('create_project')
@ns.expect(project)
@ns.marshal_with(project_response, code=201)
@ns.expect(project_input)
@ns.marshal_with(project, code=201)
def post(self):
"""Create a project"""
return project_dao.create(ns.payload), 201
Expand All @@ -71,7 +71,7 @@ def post(self):
@ns.param('uid', 'The project identifier')
class Project(Resource):
@ns.doc('get_project')
@ns.marshal_with(project_response)
@ns.marshal_with(project)
def get(self, uid):
"""Retrieve a project"""
return project_dao.get(uid)
Expand All @@ -90,8 +90,8 @@ def post(self, uid):
abort(message=str(e), code=404)

@ns.doc('put_project')
@ns.expect(project)
@ns.marshal_with(project_response)
@ns.expect(project_input)
@ns.marshal_with(project)
def put(self, uid):
"""Create or replace a project"""
return project_dao.update(uid, ns.payload)
Expand Down
20 changes: 10 additions & 10 deletions time_tracker_api/technologies/technologies_namespace.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
ns = Namespace('technologies', description='API for technologies used')

# Technology Model
technology = ns.model('Technology', {
technology_input = ns.model('TechnologyInput', {
'name': fields.String(
required=True,
title='Name',
Expand All @@ -23,24 +23,24 @@
}
technology_response_fields.update(audit_fields)

technology_response = ns.inherit(
'TechnologyResponse',
technology,
technology = ns.inherit(
'Technology',
technology_input,
technology_response_fields
)


@ns.route('')
class Technologies(Resource):
@ns.doc('list_technologies')
@ns.marshal_list_with(technology_response, code=200)
@ns.marshal_list_with(technology, code=200)
def get(self):
"""List all technologies"""
return [], 200

@ns.doc('create_technology')
@ns.expect(technology)
@ns.marshal_with(technology_response, code=201)
@ns.expect(technology_input)
@ns.marshal_with(technology, code=201)
def post(self):
"""Create a technology"""
return ns.payload, 201
Expand All @@ -51,14 +51,14 @@ def post(self):
@ns.param('uid', 'The technology identifier')
class Technology(Resource):
@ns.doc('get_technology')
@ns.marshal_with(technology_response)
@ns.marshal_with(technology)
def get(self, uid):
"""Retrieve a technology"""
return {}

@ns.doc('put_technology')
@ns.expect(technology)
@ns.marshal_with(technology_response)
@ns.expect(technology_input)
@ns.marshal_with(technology)
def put(self, uid):
"""Updates a technology"""
return ns.payload()
Expand Down
25 changes: 14 additions & 11 deletions time_tracker_api/time_entries/time_entries_namespace.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from flask_restplus import fields, Resource, Namespace
from time_tracker_api.api import audit_fields
from time_tracker_api import database

ns = Namespace('time-entries', description='API for time entries')

# TimeEntry Model
time_entry = ns.model('TimeEntry', {
time_entry_input = ns.model('TimeEntryInput', {
'project_id': fields.String(
required=True,
title='Project',
Expand Down Expand Up @@ -54,24 +55,26 @@
}
time_entry_response_fields.update(audit_fields)

time_entry_response = ns.inherit(
'TimeEntryResponse',
time_entry,
time_entry = ns.inherit(
'TimeEntry',
time_entry_input,
time_entry_response_fields,
)


model = database.create('time-entries')

@ns.route('')
class TimeEntries(Resource):
@ns.doc('list_time_entries')
@ns.marshal_list_with(time_entry_response, code=200)
@ns.marshal_list_with(time_entry, code=200)
def get(self):
"""List all available time entries"""
return []
return model.find_all()

@ns.doc('create_time_entry')
@ns.expect(time_entry)
@ns.marshal_with(time_entry_response, code=201)
@ns.expect(time_entry_input)
@ns.marshal_with(time_entry, code=201)
@ns.response(400, 'Invalid format of the attributes of the time entry')
def post(self):
"""Starts a time entry by creating it"""
Expand All @@ -83,7 +86,7 @@ def post(self):
@ns.param('id', 'The unique identifier of the time entry')
class TimeEntry(Resource):
@ns.doc('get_time_entry')
@ns.marshal_with(time_entry_response)
@ns.marshal_with(time_entry)
def get(self, id):
"""Retrieve all the data of a single time entry"""
return {}
Expand All @@ -96,8 +99,8 @@ def delete(self, id):

@ns.doc('put_time_entry')
@ns.response(400, 'Invalid format of the attributes of the time entry.')
@ns.expect(time_entry)
@ns.marshal_with(time_entry_response)
@ns.expect(time_entry_input)
@ns.marshal_with(time_entry)
def put(self, id):
"""Updates a time entry"""
return ns.payload
Expand Down
Loading