Skip to content

Commit 248070f

Browse files
committed
Initial commit
0 parents  commit 248070f

18 files changed

+432
-0
lines changed

.circleci/config.yml

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
version: 2.1
2+
3+
orbs:
4+
python: circleci/[email protected]
5+
jobs:
6+
working_directory: ~/basic_todo_list
7+
build-and-test:
8+
executor: python/default
9+
steps:
10+
- checkout
11+
- python/load-cache
12+
- python/install-deps
13+
- python/save-cache
14+
- run:
15+
command: "python -m pytest"
16+
name: Test
17+
18+
workflows:
19+
main:
20+
jobs:
21+
- build-and-test

.coverage

52 KB
Binary file not shown.

.coveragerc

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[run]
2+
omit = *venv*

.gitignore

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
venv
2+
.idea
3+
4+
venv/
5+
.idea/
6+

.travis.yml

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
language: python
2+
python:
3+
- "3.7"
4+
before-install:
5+
- "export PYTHONPATH=$PYTHONPATH:$(pwd)"
6+
install:
7+
- pip install -r requirements.txt
8+
# command to run tests
9+
script:
10+
- pytest

README.md

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# basic_todo_list
2+
[![Travis][build-badge]][build]
3+
4+
5+
[build-badge]: https://img.shields.io/travis/tomasmor42/basic_todo_list/master.png?style=flat-square
6+
[build]: https://travis-ci.org/tomasmor42/basic_todo_list
7+
8+
Basic Flask-Based to-do application.
9+
How to start:
10+
* clone this repo;
11+
* Go to the repo folder basic_todo_list;
12+
* Create a virtual environment `python -m venv venv`;
13+
* Activate it `source venv/bin/activate` (or `virtualenv venv`)
14+
* Install the `requirements pip install -r requirements.txt`;
15+
* Set PYTHONPATH with current directory: `export PYTHONPATH=current_folder`
16+
* You can start an app with python app.py. It will start at `http://127.0.0.1:5000/`
17+
* This application has following endpoint:
18+
* GET: tasks: gives all stored tasks;
19+
* GET: /tasks/<int:task_id>: gives text of the task by id
20+
* POST: /tasks/post: create a task. Takes task date (suppose to be in the future) and text in request arguments.
21+
* To run tests you can run `pytest tests/test_pytest.py`
22+
23+
24+
25+
Это небольшое приложение на Flask, которое представляет собой простйший todo-лист.
26+
Как начать:
27+
* склонировать этот репозиторий;
28+
* перейти в папку с ним;
29+
* создать виртуальное окружение `python -m venv venv`;
30+
* активировать его `source venv/bin/activate` (или `virtualenv venv`)
31+
* установить зависимости `requirements pip install -r requirements.txt`;
32+
* переменную окружения сделать равно текущей директории (текущую директорию можно узнать выполнив команду `pwd`) PYTHONPATH: `export PYTHONPATH=current_folder` (или `set PYTHONPATH=current_folder`)
33+
* Запустить приложение можно с помощью команды `python app.py`. Оно запустится по адресу `http://127.0.0.1:5000/`
34+
* У этого приложения есть следующие эндпоинты:
35+
* GET: tasks: возвращает все существующие задачи;
36+
* GET: /tasks/<int:task_id>: возвращает задачу с заданным id;
37+
* POST: /tasks/post: создает задачу. Принимает дату задачи (которая должна быть в будущем) и текст в параметрах запроса.
38+
* Запустить тесты можно с помощью команды `pytest tests/test_pytest.py`
39+
40+

__pycache__/app.cpython-38.pyc

2.81 KB
Binary file not shown.

__pycache__/service.cpython-38.pyc

953 Bytes
Binary file not shown.

app.py

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
from datetime import datetime
2+
from flask import Flask, jsonify, request
3+
4+
app = Flask(__name__)
5+
6+
from service import get_task, get_all_tasks, create_task, update_task_by_id
7+
8+
9+
class APIException(Exception):
10+
status_code = 404
11+
12+
def __init__(self, message, status_code=None, payload=None):
13+
Exception.__init__(self)
14+
self.message = message
15+
if status_code is not None:
16+
self.status_code = status_code
17+
self.payload = payload
18+
19+
def to_dict(self):
20+
rv = dict(self.payload or ())
21+
rv['message'] = self.message
22+
return rv
23+
24+
25+
@app.errorhandler(APIException)
26+
def handle_invalid_usage(error):
27+
response = jsonify(error.to_dict())
28+
response.status_code = error.status_code
29+
return response
30+
31+
32+
@app.route("/tasks", methods=['GET'])
33+
def tasks_list():
34+
tasks = get_all_tasks()
35+
return jsonify(tasks=tasks)
36+
37+
38+
@app.route("/tasks/<int:task_id>", methods=['GET'])
39+
def task_by_id(task_id):
40+
task = get_task(task_id)
41+
if not task:
42+
raise APIException("Task doesn't exist")
43+
return jsonify(task=task)
44+
45+
46+
@app.route("/tasks/<int:task_id>", methods=['PUT'])
47+
def task_update_by_id(task_id):
48+
text = request.args.get('text')
49+
update_task_by_id(task_id, text)
50+
if not task_id:
51+
raise APIException("Task doesn't exist")
52+
53+
return jsonify({'task_id': task_id})
54+
55+
56+
from uuid import uuid4
57+
from collections import namedtuple
58+
59+
TASKS = {}
60+
Task = namedtuple('Task', ['date', 'text'])
61+
62+
63+
def get_parameters_for_task_creation(args):
64+
text = args.get('text')
65+
date = args.get('date')
66+
return text, date
67+
68+
69+
def format_date(date):
70+
try:
71+
date_format = datetime.strptime(date, '%Y-%m-%d %H:%M')
72+
if date_format > datetime.now():
73+
return date_format
74+
except ValueError:
75+
return None
76+
77+
78+
@app.route("/tasks", methods=['POST'])
79+
def task_post():
80+
text, date = get_parameters_for_task_creation(request.args)
81+
if not (text and date):
82+
APIException(status_code=412, message='Text and date should not be empty')
83+
84+
date_format = format_date(date)
85+
if not date_format:
86+
raise APIException(
87+
status_code=412, message='Date should be in the future in format %Y-%m-%d %H:%M')
88+
task_id = str(uuid4())
89+
TASKS[task_id] = Task(date=date_format, text=text)
90+
91+
if task_id:
92+
return jsonify({'task_id': task_id})
93+
raise APIException(status_code=412, message='Date should be in the future')
94+
95+
96+
if __name__ == '__main__':
97+
app.run()

requirements.txt

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Flask==1.1.2
2+
pytest==5.4.3
3+
mock==4.0.2
4+
allure-pytest==2.8.16
5+
pytest-cov==2.10.0

service.py

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from collections import namedtuple
2+
from datetime import datetime
3+
from uuid import uuid4
4+
5+
Task = namedtuple('Task', ['date', 'text'])
6+
7+
TASKS = {}
8+
9+
10+
def get_task(_id):
11+
if _id in TASKS:
12+
return TASKS[_id]
13+
14+
15+
def get_all_tasks():
16+
return TASKS
17+
18+
19+
def create_task(date, text):
20+
date_format = datetime.strptime(date, '%Y-%m-%d %H:%M')
21+
if date_format > datetime.now():
22+
task_id = str(uuid4())
23+
TASKS[task_id] = Task(date=date_format, text=text)
24+
return task_id
25+
26+
27+
def update_task_by_id(task_id, text):
28+
if not task_id in TASKS:
29+
return
30+
TASKS[task_id] = text
Binary file not shown.
Binary file not shown.
1.09 KB
Binary file not shown.

tests/conftest.py

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import pytest
2+
3+
import service
4+
5+
TASK_ID = 1
6+
TASK_TEXT = "text text"
7+
TASKS = {TASK_ID: TASK_TEXT}
8+
9+
10+
@pytest.fixture()
11+
def tasks():
12+
service.TASKS = TASKS
13+
14+
15+
@pytest.fixture()
16+
def tasks_empty():
17+
service.TASKS = TASKS

tests/test_allure_features.py

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import time
2+
import pytest
3+
4+
5+
@pytest.mark.xfail()
6+
def test_xfail():
7+
assert False
8+
9+
10+
@pytest.mark.xfail()
11+
def test_xpass():
12+
assert False
13+
14+
15+
def test_unexpected_fail():
16+
assert False
17+
18+
19+
@pytest.mark.parametrize('param1', [0, 1, 2])
20+
@pytest.mark.parametrize('param2', ['a', 'b', 'c'])
21+
def test_parametrized(param1, param2):
22+
assert param1
23+
assert param2
24+
25+
26+
@pytest.mark.skip()
27+
def test_skip():
28+
assert True
29+
30+
31+
def test_long_test():
32+
time.sleep(3)
33+
assert True

tests/test_pytest.py

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import datetime
2+
from mock import MagicMock
3+
4+
from app import get_parameters_for_task_creation, format_date
5+
6+
import pytest
7+
8+
import service
9+
10+
TASK_ID = 1
11+
TASK_TEXT = "text text"
12+
TASKS = {TASK_ID: TASK_TEXT}
13+
14+
15+
def test_get_task_id_exists(tasks):
16+
result_task = service.get_task(TASK_ID)
17+
assert result_task == TASK_TEXT
18+
19+
20+
def test_get_task_doesnt_exist(tasks):
21+
result_task = service.get_task(2)
22+
assert result_task is None
23+
24+
25+
def test_get_all_tasks_empty(tasks_empty):
26+
service.TASKS = {}
27+
all_tasks = service.get_all_tasks()
28+
assert all_tasks == {}
29+
service.TASKS = TASKS
30+
31+
32+
def test_get_all_tasks_not_empty(tasks):
33+
all_tasks = service.get_all_tasks()
34+
assert all_tasks == TASKS
35+
36+
37+
def test_create_task_success():
38+
date = (
39+
datetime.datetime.now() +
40+
datetime.timedelta(days=1)).strftime("%Y-%m-%d %H:%M")
41+
task = service.create_task(date, TASK_TEXT)
42+
assert task
43+
44+
45+
def test_create_task_in_the_past():
46+
date = (
47+
datetime.datetime.now() -
48+
datetime.timedelta(days=1)).strftime("%Y-%m-%d %H:%M")
49+
50+
task = service.create_task(date, TASK_TEXT)
51+
assert task is None
52+
53+
54+
def test_get_parameters_ok():
55+
args = MagicMock(date="date", text="text")
56+
text, date = get_parameters_for_task_creation(args)
57+
assert text == text
58+
assert date == date
59+
60+
61+
def test_get_parameters_invalid():
62+
args = MagicMock()
63+
args.get.side_effect = ['text', None]
64+
text, date = get_parameters_for_task_creation(args)
65+
assert date is None
66+
assert text == text
67+
68+
69+
def test_format_date_valid():
70+
date = '2102-01-01 12:12'
71+
res = format_date(date)
72+
assert res == datetime.datetime(2102, 1, 1, 12, 12)
73+
74+
75+
@pytest.mark.parametrize('invalid_date', ['2012-01-01 12:12', '2102-01-01'])
76+
def test_format_date_invalid(invalid_date):
77+
res = format_date(invalid_date)
78+
assert res is None
79+
80+
81+
# ===================================================================

0 commit comments

Comments
 (0)