Skip to content

Commit 8e287d8

Browse files
author
EliuX
committed
Close #52 Implement project model in Cosmos DB
1 parent 6571f32 commit 8e287d8

File tree

20 files changed

+149
-170
lines changed

20 files changed

+149
-170
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
# time-tracker-api
22

3+
[![Build status](https://dev.azure.com/IOET-DevOps/TimeTracker-API/_apis/build/status/TimeTracker-API%20-%20CI)](https://dev.azure.com/IOET-DevOps/TimeTracker-API/_build/latest?definitionId=1)
4+
35
This is the mono-repository for the backend services and their common codebase
46

7+
8+
59
## Getting started
610
Follow the following instructions to get the project ready to use ASAP.
711

cli.py

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -46,23 +46,6 @@ def gen_postman_collection(filename='timetracker-api-postman-collection.json',
4646
save_data(parsed_json, filename)
4747

4848

49-
@cli_manager.command
50-
def seed():
51-
from time_tracker_api.database import seeder as seed
52-
seed()
53-
54-
55-
@cli_manager.command
56-
def re_create_db():
57-
print('This is going to drop all tables and seed again the database')
58-
confirm_answer = input('Do you confirm (Y) you want to remove all your data?\n')
59-
if confirm_answer.upper() == 'Y':
60-
from time_tracker_api.database import seeder
61-
seeder.fresh()
62-
else:
63-
print('\nThis action was cancelled!')
64-
65-
6649
def save_data(data: str, filename: str) -> None:
6750
""" Save text content to a file """
6851
if filename:

commons/data_access_layer/cosmos_db.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
from azure.cosmos import ContainerProxy, PartitionKey
99
from flask import Flask
1010

11+
from commons.data_access_layer.database import CRUDDao
12+
from time_tracker_api.security import current_user_tenant_id
13+
1114

1215
class CosmosDBFacade:
1316
def __init__(self, client, db_id: str, logger=None): # pragma: no cover
@@ -122,7 +125,7 @@ def find_all(self, partition_key_value: str, max_count=None, offset=0,
122125

123126
def partial_update(self, id: str, changes: dict, partition_key_value: str,
124127
visible_only=True, mapper: Callable = None):
125-
item_data = self.find(id, partition_key_value, visible_only=visible_only)
128+
item_data = self.find(id, partition_key_value, visible_only=visible_only, mapper=dict)
126129
item_data.update(changes)
127130
return self.update(id, item_data, mapper=mapper)
128131

@@ -160,6 +163,34 @@ def get_page_size_or(self, custom_page_size: int) -> int:
160163
return custom_page_size or 100
161164

162165

166+
class CosmosDBDao(CRUDDao):
167+
def __init__(self, repository: CosmosDBRepository):
168+
self.repository = repository
169+
170+
def get_all(self) -> list:
171+
tenant_id: str = current_user_tenant_id()
172+
return self.repository.find_all(partition_key_value=tenant_id)
173+
174+
def get(self, id):
175+
tenant_id: str = current_user_tenant_id()
176+
return self.repository.find(id, partition_key_value=tenant_id)
177+
178+
def create(self, data: dict):
179+
data['id'] = str(uuid.uuid4())
180+
data['tenant_id'] = current_user_tenant_id()
181+
return self.repository.create(data)
182+
183+
def update(self, id, data: dict):
184+
tenant_id: str = current_user_tenant_id()
185+
return self.repository.partial_update(id,
186+
changes=data,
187+
partition_key_value=tenant_id)
188+
189+
def delete(self, id):
190+
tenant_id: str = current_user_tenant_id()
191+
self.repository.delete(id, partition_key_value=tenant_id)
192+
193+
163194
def init_app(app: Flask) -> None:
164195
global cosmos_helper
165196
cosmos_helper = CosmosDBFacade.from_flask_config(app)

time_tracker_api/database.py renamed to commons/data_access_layer/database.py

Lines changed: 5 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -35,45 +35,16 @@ def delete(self, id):
3535
raise NotImplementedError # pragma: no cover
3636

3737

38-
class Seeder(abc.ABC):
39-
@abc.abstractmethod
40-
def run(self):
41-
raise NotImplementedError # pragma: no cover
42-
43-
@abc.abstractmethod
44-
def fresh(self):
45-
raise NotImplementedError # pragma: no cover
46-
47-
def __call__(self, *args, **kwargs):
48-
self.run() # pragma: no cover
49-
50-
51-
seeder: Seeder = None
52-
53-
5438
def init_app(app: Flask) -> None:
55-
init_sql(app)
39+
init_sql(app) # TODO Delete after the migration to Cosmos DB has finished.
40+
init_cosmos_db(app)
5641

5742

5843
def init_sql(app: Flask) -> None:
59-
from commons.data_access_layer.sql import init_app, SQLSeeder
44+
from commons.data_access_layer.sql import init_app
6045
init_app(app)
61-
global seeder
62-
seeder = SQLSeeder()
6346

6447

6548
def init_cosmos_db(app: Flask) -> None:
66-
# from commons.data_access_layer.azure.cosmos_db import cosmos_helper
67-
class CosmosSeeder(Seeder):
68-
def run(self):
69-
print("Provisioning namespace(database)...")
70-
# cosmos_helper.create_container()
71-
print("Database seeded!")
72-
73-
def fresh(self):
74-
print("Removing namespace(database)...")
75-
# cosmos_helper.remove_container()
76-
self.run()
77-
78-
global seeder
79-
seeder = CosmosSeeder()
49+
from commons.data_access_layer.cosmos_db import init_app
50+
init_app(app)

commons/data_access_layer/sql.py

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from flask import Flask
44
from flask_sqlalchemy import SQLAlchemy
55

6-
from time_tracker_api.database import CRUDDao, Seeder, ID_MAX_LENGTH
6+
from commons.data_access_layer.database import CRUDDao, ID_MAX_LENGTH
77
from time_tracker_api.security import current_user_id
88

99
db: SQLAlchemy = None
@@ -14,7 +14,7 @@ def handle_commit_issues(f):
1414
def rollback_if_necessary(*args, **kw):
1515
try:
1616
return f(*args, **kw)
17-
except: # pragma: no cover
17+
except: # pragma: no cover
1818
db.session.rollback()
1919
raise
2020

@@ -90,16 +90,3 @@ def update(self, id, data: dict):
9090

9191
def delete(self, id):
9292
self.repository.remove(id)
93-
94-
95-
class SQLSeeder(Seeder): # pragma: no cover
96-
def run(self):
97-
print("Provisioning database...")
98-
db.create_all()
99-
print("Database seeded!")
100-
101-
def fresh(self):
102-
print("Removing all existing data...")
103-
db.drop_all()
104-
105-
self.run()

migrations/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,4 +74,4 @@ def remove_migration(self, name):
7474
self.repository.delete_permanently(name, self.app_id)
7575

7676

77-
configure(storage=CosmosDBStorage("migrations", "time-tracker-api"))
77+
configure(storage=CosmosDBStorage("migration", "time-tracker-api"))

requirements/migrations.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@
33
# For running any kind of data migration
44

55
# Migration tool
6-
migrate-anything==0.1.6
6+
migrate-anything==0.1.6

requirements/time_tracker_api/dev.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@ pytest==5.2.0
1212
pytest-mock==2.0.0
1313

1414
# Coverage
15-
coverage==4.5.1
15+
coverage==4.5.1

tests/time_tracker_api/projects/projects_namespace_test.py

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,14 @@
44
from flask_restplus._http import HTTPStatus
55
from pytest_mock import MockFixture
66

7-
from time_tracker_api.projects.projects_model import PROJECT_TYPE
7+
from time_tracker_api.security import current_user_tenant_id
88

99
fake = Faker()
1010

1111
valid_project_data = {
1212
"name": fake.company(),
1313
"description": fake.paragraph(),
1414
'customer_id': fake.uuid4(),
15-
'tenant_id': fake.uuid4(),
1615
'project_type_id': fake.uuid4()
1716
}
1817

@@ -30,7 +29,7 @@ def test_create_project_should_succeed_with_valid_request(client: FlaskClient, m
3029
response = client.post("/projects", json=valid_project_data, follow_redirects=True)
3130

3231
assert HTTPStatus.CREATED == response.status_code
33-
repository_create_mock.assert_called_once_with(valid_project_data)
32+
repository_create_mock.assert_called_once()
3433

3534

3635
def test_create_project_should_reject_bad_request(client: FlaskClient, mocker: MockFixture):
@@ -73,7 +72,8 @@ def test_get_project_should_succeed_with_valid_id(client: FlaskClient, mocker: M
7372

7473
assert HTTPStatus.OK == response.status_code
7574
fake_project == json.loads(response.data)
76-
repository_find_mock.assert_called_once_with(str(valid_id))
75+
repository_find_mock.assert_called_once_with(str(valid_id),
76+
partition_key_value=current_user_tenant_id())
7777

7878

7979
def test_get_project_should_return_not_found_with_invalid_id(client: FlaskClient, mocker: MockFixture):
@@ -89,7 +89,8 @@ def test_get_project_should_return_not_found_with_invalid_id(client: FlaskClient
8989
response = client.get("/projects/%s" % invalid_id, follow_redirects=True)
9090

9191
assert HTTPStatus.NOT_FOUND == response.status_code
92-
repository_find_mock.assert_called_once_with(str(invalid_id))
92+
repository_find_mock.assert_called_once_with(str(invalid_id),
93+
partition_key_value=current_user_tenant_id())
9394

9495

9596
def test_get_project_should_response_with_unprocessable_entity_for_invalid_id_format(client: FlaskClient,
@@ -106,22 +107,25 @@ def test_get_project_should_response_with_unprocessable_entity_for_invalid_id_fo
106107
response = client.get("/projects/%s" % invalid_id, follow_redirects=True)
107108

108109
assert HTTPStatus.UNPROCESSABLE_ENTITY == response.status_code
109-
repository_find_mock.assert_called_once_with(str(invalid_id))
110+
repository_find_mock.assert_called_once_with(str(invalid_id),
111+
partition_key_value=current_user_tenant_id())
110112

111113

112114
def test_update_project_should_succeed_with_valid_data(client: FlaskClient, mocker: MockFixture):
113115
from time_tracker_api.projects.projects_namespace import project_dao
114116

115117
repository_update_mock = mocker.patch.object(project_dao.repository,
116-
'update',
118+
'partial_update',
117119
return_value=fake_project)
118120

119121
valid_id = fake.random_int(1, 9999)
120122
response = client.put("/projects/%s" % valid_id, json=valid_project_data, follow_redirects=True)
121123

122124
assert HTTPStatus.OK == response.status_code
123125
fake_project == json.loads(response.data)
124-
repository_update_mock.assert_called_once_with(str(valid_id), valid_project_data)
126+
repository_update_mock.assert_called_once_with(str(valid_id),
127+
changes=valid_project_data,
128+
partition_key_value=current_user_tenant_id())
125129

126130

127131
def test_update_project_should_reject_bad_request(client: FlaskClient, mocker: MockFixture):
@@ -148,15 +152,17 @@ def test_update_project_should_return_not_found_with_invalid_id(client: FlaskCli
148152
invalid_id = fake.random_int(1, 9999)
149153

150154
repository_update_mock = mocker.patch.object(project_dao.repository,
151-
'update',
155+
'partial_update',
152156
side_effect=NotFound)
153157

154158
response = client.put("/projects/%s" % invalid_id,
155159
json=valid_project_data,
156160
follow_redirects=True)
157161

158162
assert HTTPStatus.NOT_FOUND == response.status_code
159-
repository_update_mock.assert_called_once_with(str(invalid_id), valid_project_data)
163+
repository_update_mock.assert_called_once_with(str(invalid_id),
164+
changes=valid_project_data,
165+
partition_key_value=current_user_tenant_id())
160166

161167

162168
def test_delete_project_should_succeed_with_valid_id(client: FlaskClient, mocker: MockFixture):
@@ -165,14 +171,15 @@ def test_delete_project_should_succeed_with_valid_id(client: FlaskClient, mocker
165171
valid_id = fake.random_int(1, 9999)
166172

167173
repository_remove_mock = mocker.patch.object(project_dao.repository,
168-
'remove',
174+
'delete',
169175
return_value=None)
170176

171177
response = client.delete("/projects/%s" % valid_id, follow_redirects=True)
172178

173179
assert HTTPStatus.NO_CONTENT == response.status_code
174180
assert b'' == response.data
175-
repository_remove_mock.assert_called_once_with(str(valid_id))
181+
repository_remove_mock.assert_called_once_with(str(valid_id),
182+
partition_key_value=current_user_tenant_id())
176183

177184

178185
def test_delete_project_should_return_not_found_with_invalid_id(client: FlaskClient, mocker: MockFixture):
@@ -182,13 +189,14 @@ def test_delete_project_should_return_not_found_with_invalid_id(client: FlaskCli
182189
invalid_id = fake.random_int(1, 9999)
183190

184191
repository_remove_mock = mocker.patch.object(project_dao.repository,
185-
'remove',
192+
'delete',
186193
side_effect=NotFound)
187194

188195
response = client.delete("/projects/%s" % invalid_id, follow_redirects=True)
189196

190197
assert HTTPStatus.NOT_FOUND == response.status_code
191-
repository_remove_mock.assert_called_once_with(str(invalid_id))
198+
repository_remove_mock.assert_called_once_with(str(invalid_id),
199+
partition_key_value=current_user_tenant_id())
192200

193201

194202
def test_delete_project_should_return_unprocessable_entity_for_invalid_id_format(client: FlaskClient,
@@ -199,10 +207,11 @@ def test_delete_project_should_return_unprocessable_entity_for_invalid_id_format
199207
invalid_id = fake.company()
200208

201209
repository_remove_mock = mocker.patch.object(project_dao.repository,
202-
'remove',
210+
'delete',
203211
side_effect=UnprocessableEntity)
204212

205213
response = client.delete("/projects/%s" % invalid_id, follow_redirects=True)
206214

207215
assert HTTPStatus.UNPROCESSABLE_ENTITY == response.status_code
208-
repository_remove_mock.assert_called_once_with(str(invalid_id))
216+
repository_remove_mock.assert_called_once_with(str(invalid_id),
217+
partition_key_value=current_user_tenant_id())

tests/time_tracker_api/smoke_test.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
import pyodbc
2-
31
import pytest
2+
from azure.cosmos.exceptions import CosmosHttpResponseError, CosmosResourceExistsError, CosmosResourceNotFoundError
43
from flask.testing import FlaskClient
54
from flask_restplus._http import HTTPStatus
65
from pytest_mock import MockFixture
76

8-
unexpected_errors_to_be_handled = [pyodbc.OperationalError]
7+
unexpected_errors_to_be_handled = [CosmosHttpResponseError, CosmosResourceNotFoundError, CosmosResourceExistsError]
98

109

1110
def test_app_exists(app):

0 commit comments

Comments
 (0)