Skip to content

Commit 8d2bc90

Browse files
author
EliuX
committed
feat: Close #101 #53 Add time_tracker_events and bind it to CosmosDB to track events
1 parent 495190d commit 8d2bc90

File tree

14 files changed

+238
-25
lines changed

14 files changed

+238
-25
lines changed

.DS_Store

6 KB
Binary file not shown.

commons/data_access_layer/cosmos_db.py

Lines changed: 51 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@
1010
from flask import Flask
1111
from werkzeug.exceptions import HTTPException
1212

13-
from commons.data_access_layer.database import CRUDDao
14-
from time_tracker_api.security import current_user_tenant_id
13+
from commons.data_access_layer.database import CRUDDao, EventContext
1514

1615

1716
class CosmosDBFacade:
@@ -140,23 +139,35 @@ def replace_empty_value_per_none(item_data: dict) -> dict:
140139
if isinstance(v, str) and len(v) == 0:
141140
item_data[k] = None
142141

143-
def create(self, data: dict, mapper: Callable = None):
144-
self.on_create(data)
142+
def attach_context(data: dict, event_context: EventContext):
143+
data["_last_event_ctx"] = {
144+
"user_id": event_context.user_id,
145+
"tenant_id": event_context.tenant_id,
146+
"action": event_context.action,
147+
"description": event_context.description,
148+
"container_id": event_context.container_id,
149+
"session_id": event_context.session_id,
150+
}
151+
152+
def create(self, data: dict, event_context: EventContext, mapper: Callable = None):
153+
self.on_create(data, event_context)
145154
function_mapper = self.get_mapper_or_dict(mapper)
155+
self.attach_context(data, event_context)
146156
return function_mapper(self.container.create_item(body=data))
147157

148-
def find(self, id: str, partition_key_value, peeker: 'function' = None,
158+
def find(self, id: str, event_context: EventContext, peeker: 'function' = None,
149159
visible_only=True, mapper: Callable = None):
160+
partition_key_value = self.find_partition_key_value(event_context)
150161
found_item = self.container.read_item(id, partition_key_value)
151162
if peeker:
152163
peeker(found_item)
153164

154165
function_mapper = self.get_mapper_or_dict(mapper)
155166
return function_mapper(self.check_visibility(found_item, visible_only))
156167

157-
def find_all(self, partition_key_value: str, conditions: dict = {}, max_count=None, offset=0,
158-
visible_only=True, mapper: Callable = None):
159-
# TODO Use the tenant_id param and change container alias
168+
def find_all(self, event_context: EventContext, conditions: dict = {}, max_count=None,
169+
offset=0, visible_only=True, mapper: Callable = None):
170+
partition_key_value = self.find_partition_key_value(event_context)
160171
max_count = self.get_page_size_or(max_count)
161172
params = [
162173
{"name": "@partition_key_value", "value": partition_key_value},
@@ -179,27 +190,32 @@ def find_all(self, partition_key_value: str, conditions: dict = {}, max_count=No
179190
function_mapper = self.get_mapper_or_dict(mapper)
180191
return list(map(function_mapper, result))
181192

182-
def partial_update(self, id: str, changes: dict, partition_key_value: str,
193+
def partial_update(self, id: str, changes: dict, event_context: EventContext,
183194
peeker: 'function' = None, visible_only=True, mapper: Callable = None):
184-
item_data = self.find(id, partition_key_value, peeker=peeker,
185-
visible_only=visible_only, mapper=dict)
195+
item_data = self.find(id, event_context, peeker=peeker, visible_only=visible_only, mapper=dict)
186196
item_data.update(changes)
187-
return self.update(id, item_data, mapper=mapper)
197+
return self.update(id, item_data, event_context=event_context, mapper=mapper)
188198

189-
def update(self, id: str, item_data: dict, mapper: Callable = None):
199+
def update(self, id: str, item_data: dict, event_context: EventContext,
200+
mapper: Callable = None):
190201
self.on_update(item_data)
191202
function_mapper = self.get_mapper_or_dict(mapper)
203+
self.attach_context(item_data, event_context)
192204
return function_mapper(self.container.replace_item(id, body=item_data))
193205

194-
def delete(self, id: str, partition_key_value: str,
206+
def delete(self, id: str, event_context: EventContext,
195207
peeker: 'function' = None, mapper: Callable = None):
208+
partition_key_value = self.find_partition_key_value(event_context)
196209
return self.partial_update(id, {
197210
'deleted': str(uuid.uuid4())
198211
}, partition_key_value, peeker=peeker, visible_only=True, mapper=mapper)
199212

200213
def delete_permanently(self, id: str, partition_key_value: str) -> None:
201214
self.container.delete_item(id, partition_key_value)
202215

216+
def find_partition_key_value(self, event_context: EventContext):
217+
return getattr(event_context, self.partition_key_attribute)
218+
203219
def get_mapper_or_dict(self, alternative_mapper: Callable) -> Callable:
204220
return alternative_mapper or self.mapper or dict
205221

@@ -208,10 +224,12 @@ def get_page_size_or(self, custom_page_size: int) -> int:
208224
# or any other repository for the settings
209225
return custom_page_size or 100
210226

211-
def on_create(self, new_item_data: dict):
227+
def on_create(self, new_item_data: dict, event_context: EventContext):
212228
if new_item_data.get('id') is None:
213229
new_item_data['id'] = generate_uuid4()
214230

231+
new_item_data[self.partition_key_attribute] = self.find_partition_key_value(event_context)
232+
215233
self.replace_empty_value_per_none(new_item_data)
216234

217235
def on_update(self, update_item_data: dict):
@@ -228,27 +246,35 @@ def __init__(self, repository: CosmosDBRepository):
228246
self.repository = repository
229247

230248
def get_all(self, conditions: dict = {}) -> list:
231-
return self.repository.find_all(partition_key_value=self.partition_key_value,
249+
event_ctx = self.create_event_context("read-many")
250+
return self.repository.find_all(event_context=event_ctx,
232251
conditions=conditions)
233252

234253
def get(self, id):
235-
return self.repository.find(id, partition_key_value=self.partition_key_value)
254+
event_ctx = self.create_event_context("read")
255+
return self.repository.find(id, event_context=event_ctx)
236256

237257
def create(self, data: dict):
238-
data[self.repository.partition_key_attribute] = self.partition_key_value
239-
return self.repository.create(data)
258+
event_ctx = self.create_event_context("create")
259+
return self.repository.create(data, event_context=event_ctx)
240260

241261
def update(self, id, data: dict):
242-
return self.repository.partial_update(id,
243-
changes=data,
244-
partition_key_value=self.partition_key_value)
262+
event_ctx = self.create_event_context("update")
263+
return self.repository.partial_update(id, changes=data, event_context=event_ctx)
245264

246265
def delete(self, id):
247-
self.repository.delete(id, partition_key_value=self.partition_key_value)
266+
event_ctx = self.create_event_context("update")
267+
self.repository.delete(id, event_context=event_ctx)
248268

249269
@property
250-
def partition_key_value(self):
251-
return current_user_tenant_id()
270+
def find_partition_key_value(self, event_context: EventContext):
271+
return event_context.tenant_id
272+
273+
# Replace by decorator and put it in the repository
274+
def create_event_context(self, action: str = None, description: str = None):
275+
return EventContext(self.repository.container.id,
276+
action,
277+
description=description)
252278

253279

254280
class CustomError(HTTPException):

commons/data_access_layer/database.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
from flask import Flask
1111

12+
from time_tracker_api.security import current_user_id, current_user_tenant_id
13+
1214
COMMENTS_MAX_LENGTH = 500
1315
ID_MAX_LENGTH = 64
1416

@@ -35,6 +37,33 @@ def delete(self, id):
3537
raise NotImplementedError # pragma: no cover
3638

3739

40+
class EventContext:
41+
def __init__(self, container_id: str, action: str, description: str = None,
42+
user_id: str = None, tenant_id: str = None, session_id: str = None):
43+
self.container_id = container_id
44+
self.action = action
45+
self.description = description
46+
self._user_id = user_id
47+
self._tenant_id = tenant_id
48+
self._session_id = session_id
49+
50+
@property
51+
def user_id(self) -> str:
52+
if self._user_id is None:
53+
self._user_id = current_user_id()
54+
return self._user_id
55+
56+
@property
57+
def tenant_id(self) -> str:
58+
if self._tenant_id is None:
59+
self._tenant_id = current_user_tenant_id()
60+
return self._tenant_id
61+
62+
@property
63+
def session_id(self) -> str:
64+
return self._session_id
65+
66+
3867
def init_app(app: Flask) -> None:
3968
init_cosmos_db(app)
4069

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from azure.cosmos import PartitionKey
2+
3+
container_definition = {
4+
'id': 'event',
5+
'partition_key': PartitionKey(path='/tenant_id')
6+
}

migrations/02-add-events.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
def up():
2+
from commons.data_access_layer.cosmos_db import cosmos_helper
3+
import azure.cosmos.exceptions as exceptions
4+
from commons.data_access_layer.events_model import container_definition as event_definition
5+
from . import app
6+
7+
app.logger.info("Creating container events...")
8+
9+
try:
10+
app.logger.info('- Event')
11+
cosmos_helper.create_container(event_definition)
12+
except exceptions.CosmosResourceExistsError as e:
13+
app.logger.warning("Unexpected error while creating container for events: %s" % e.message)
14+
15+
app.logger.info("Done!")
16+
17+
18+
def down():
19+
print("Not implemented!")
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# requirements/time_tracker_events/dev.txt
2+
3+
4+
# Include the prod resources
5+
-r prod.txt
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# requirements/time_tracker_events/prod.txt
2+
3+
# Azure Functions library
4+
azure-functions
5+

time_tracker_events/.funcignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

time_tracker_events/.gitignore

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
bin
2+
obj
3+
csx
4+
.vs
5+
edge
6+
Publish
7+
8+
*.user
9+
*.suo
10+
*.cscfg
11+
*.Cache
12+
project.lock.json
13+
14+
/packages
15+
/TestResults
16+
17+
/tools/NuGet.exe
18+
/App_Data
19+
/secrets
20+
/data
21+
.secrets
22+
appsettings.json
23+
local.settings.json
24+
25+
node_modules
26+
dist
27+
28+
# Local python packages
29+
.python_packages/
30+
31+
# Python Environments
32+
.env
33+
.venv
34+
env/
35+
venv/
36+
ENV/
37+
env.bak/
38+
venv.bak/
39+
40+
# Byte-compiled / optimized / DLL files
41+
__pycache__/
42+
*.py[cod]
43+
*$py.class
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import logging
2+
import uuid
3+
from datetime import datetime
4+
5+
import azure.functions as func
6+
7+
8+
def main(documents: func.DocumentList, events: func.Out[func.Document]):
9+
if documents:
10+
new_events = func.DocumentList()
11+
12+
for doc in documents:
13+
logging.info(doc.to_json())
14+
15+
event_context = doc.get("_last_event_ctx")
16+
if event_context is not None:
17+
new_events.append(func.Document.from_dict({
18+
"id": str(uuid.uuid4()),
19+
"date": datetime.utcnow().isoformat(),
20+
"user_id": event_context.get("user_id"),
21+
"action": event_context.get("action"),
22+
"description": event_context.get("description"),
23+
"item_id": doc.get("id"),
24+
"container_id": event_context.get("container_id"),
25+
"session_id": event_context.get("session_id"),
26+
"tenant_id": event_context.get("tenant_id"),
27+
}))
28+
else:
29+
logging.warning("- Not saved!")
30+
31+
if len(new_events):
32+
events.set(new_events)
33+
else:
34+
logging.warning("No valid events were found!")

0 commit comments

Comments
 (0)