diff --git a/.gitignore b/.gitignore index 7ac18d21..c6f8a483 100644 --- a/.gitignore +++ b/.gitignore @@ -12,5 +12,11 @@ __pycache__/ .vs .trash +# Jetbrans IDEs +.idea + # virtual environments -.venv \ No newline at end of file +.venv + +# Files generated for development +.env diff --git a/README.md b/README.md index 1cf9aea2..87c1760f 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,39 @@ # time-tracker-api -## Set new environment in Windows -- `mkdir .venv` -- `python -m venv .venv` -- `.venv\Scripts\activate.bat` -- `pip install -r requirements/prod.txt` - -## Example usage in Windows -- `set FLASK_APP=time_tracker_api` -- `flask run` +## Setup + +- Create and activate the environment, + + In Windows: + + ``` + python -m venv .venv + .venv\Scripts\activate.bat + ``` + + In Unix based operative systems: + ``` + virtualenv .venv + source .venv/bin/activate + ``` +- Install the requirements: + ``` + python3 -m pip install -r requirements/prod.txt + ``` + Remember to do it with Python 3. + +## How to use it +- Set the env var `FLASK_APP` to `time_tracker_api` and start the app: + + In Windows + ``` + set FLASK_APP=time_tracker_api + flask run + ``` + In Unix based operative systems: + ``` + export FLASK_APP=time_tracker_api + flask run + ``` + - Open `http://127.0.0.1:5000/` in a browser \ No newline at end of file diff --git a/requirements/prod.txt b/requirements/prod.txt index c14dcd97..80d3c5e9 100644 --- a/requirements/prod.txt +++ b/requirements/prod.txt @@ -1,4 +1,5 @@ Flask==1.1.1 flask-restplus==0.13.0 flake8==3.7.9 -Werkzeug==0.16.1 \ No newline at end of file +Werkzeug==0.16.1 +gunicorn==20.0.4 \ No newline at end of file diff --git a/run.py b/run.py new file mode 100644 index 00000000..37d13f83 --- /dev/null +++ b/run.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python3 +""" +This file is needed by gunicorn to run +""" +from time_tracker_api import create_app + +app = create_app() +print("BPM Projects API server was created") diff --git a/time_tracker_api/errors.py b/time_tracker_api/errors.py new file mode 100644 index 00000000..8221c103 --- /dev/null +++ b/time_tracker_api/errors.py @@ -0,0 +1,19 @@ +class MissingResource(Exception): + """ + Errors related to missing resource in the system + """ + pass + + +class InvalidInput(Exception): + """ + Errors related to an invalid input coming from the user + """ + pass + + +class InvalidMatch(Exception): + """ + Errors related to an invalid match during a search + """ + pass diff --git a/time_tracker_api/projects/projects_endpoints.py b/time_tracker_api/projects/projects_endpoints.py index a1275aa8..eabc6fa8 100644 --- a/time_tracker_api/projects/projects_endpoints.py +++ b/time_tracker_api/projects/projects_endpoints.py @@ -1,22 +1,27 @@ -from flask_restplus import Namespace -from flask_restplus import Resource +from flask_restplus import Namespace, Resource, abort, inputs +from .projects_model import project_dao +from time_tracker_api.errors import MissingResource ns = Namespace('projects', description='Api for resource `Projects`') - @ns.route('/') class Projects(Resource): @ns.doc('list_projects') def get(self): """List all projects""" - print("List all projects") - return {}, 200 + return project_dao.get_all(), 200 @ns.doc('create_project') def post(self): """Create a project""" - print("List all projects") - return {}, 201 + return project_dao.create(ns.payload), 201 + +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('/') @@ -26,26 +31,29 @@ class Project(Resource): @ns.doc('get_project') def get(self, uid): """Retrieve a project""" - print("Retrieve Project") - return {} + return project_dao.get(uid) @ns.doc('delete_project') @ns.response(204, 'Project deleted') def delete(self, uid): """Deletes a project""" - print("Delete Project") + project_dao.delete(uid) return None, 204 @ns.doc('put_project') def put(self, uid): """Create or replace a project""" - print("Create or Replace Project") - return {} + return project_dao.update(uid, ns.payload) @ns.doc('update_project_status') @ns.param('uid', 'The project identifier') @ns.response(204, 'State of the project successfully updated') def post(self, uid): """Updates a project using form data""" - print("Update Project using form data") - return {} + try: + update_data = project_update_parser.parse_args() + return project_dao.update(uid, update_data), 200 + except ValueError: + abort(code=400) + except MissingResource as e: + abort(message=str(e), code=404) diff --git a/time_tracker_api/projects/projects_model.py b/time_tracker_api/projects/projects_model.py new file mode 100644 index 00000000..e31ad5cb --- /dev/null +++ b/time_tracker_api/projects/projects_model.py @@ -0,0 +1,80 @@ +from time_tracker_api.errors \ + import MissingResource, InvalidInput, InvalidMatch + + +class InMemoryProjectDAO(object): + def __init__(self): + self.counter = 0 + self.projects = [] + + def get_all(self): + return self.projects + + def get(self, uid): + for project in self.projects: + if project.get('uid') == uid: + return project + raise MissingResource("Project '%s' not found" % uid) + + def create(self, project): + self.counter += 1 + project['uid'] = str(self.counter) + self.projects.append(project) + return project + + def update(self, uid, data): + project = self.get(uid) + if project: + project.update(data) + return project + else: + raise MissingResource("Project '%s' not found" % uid) + + def delete(self, uid): + if uid: + project = self.get(uid) + 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()