Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
feat: Close #101 #53 Add time_tracker_events and bind it to CosmosDB …
…to track events
  • Loading branch information
EliuX committed Apr 30, 2020
commit 8d2bc90f5fe722b64da37cda16944671574e2d55
Binary file added .DS_Store
Binary file not shown.
76 changes: 51 additions & 25 deletions commons/data_access_layer/cosmos_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@
from flask import Flask
from werkzeug.exceptions import HTTPException

from commons.data_access_layer.database import CRUDDao
from time_tracker_api.security import current_user_tenant_id
from commons.data_access_layer.database import CRUDDao, EventContext


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

def create(self, data: dict, mapper: Callable = None):
self.on_create(data)
def attach_context(data: dict, event_context: EventContext):
data["_last_event_ctx"] = {
"user_id": event_context.user_id,
"tenant_id": event_context.tenant_id,
"action": event_context.action,
"description": event_context.description,
"container_id": event_context.container_id,
"session_id": event_context.session_id,
}

def create(self, data: dict, event_context: EventContext, mapper: Callable = None):
self.on_create(data, event_context)
function_mapper = self.get_mapper_or_dict(mapper)
self.attach_context(data, event_context)
return function_mapper(self.container.create_item(body=data))

def find(self, id: str, partition_key_value, peeker: 'function' = None,
def find(self, id: str, event_context: EventContext, peeker: 'function' = None,
visible_only=True, mapper: Callable = None):
partition_key_value = self.find_partition_key_value(event_context)
found_item = self.container.read_item(id, partition_key_value)
if peeker:
peeker(found_item)

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

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

def partial_update(self, id: str, changes: dict, partition_key_value: str,
def partial_update(self, id: str, changes: dict, event_context: EventContext,
peeker: 'function' = None, visible_only=True, mapper: Callable = None):
item_data = self.find(id, partition_key_value, peeker=peeker,
visible_only=visible_only, mapper=dict)
item_data = self.find(id, event_context, peeker=peeker, visible_only=visible_only, mapper=dict)
item_data.update(changes)
return self.update(id, item_data, mapper=mapper)
return self.update(id, item_data, event_context=event_context, mapper=mapper)

def update(self, id: str, item_data: dict, mapper: Callable = None):
def update(self, id: str, item_data: dict, event_context: EventContext,
mapper: Callable = None):
self.on_update(item_data)
function_mapper = self.get_mapper_or_dict(mapper)
self.attach_context(item_data, event_context)
return function_mapper(self.container.replace_item(id, body=item_data))

def delete(self, id: str, partition_key_value: str,
def delete(self, id: str, event_context: EventContext,
peeker: 'function' = None, mapper: Callable = None):
partition_key_value = self.find_partition_key_value(event_context)
return self.partial_update(id, {
'deleted': str(uuid.uuid4())
}, partition_key_value, peeker=peeker, visible_only=True, mapper=mapper)

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

def find_partition_key_value(self, event_context: EventContext):
return getattr(event_context, self.partition_key_attribute)

def get_mapper_or_dict(self, alternative_mapper: Callable) -> Callable:
return alternative_mapper or self.mapper or dict

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

def on_create(self, new_item_data: dict):
def on_create(self, new_item_data: dict, event_context: EventContext):
if new_item_data.get('id') is None:
new_item_data['id'] = generate_uuid4()

new_item_data[self.partition_key_attribute] = self.find_partition_key_value(event_context)

self.replace_empty_value_per_none(new_item_data)

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

def get_all(self, conditions: dict = {}) -> list:
return self.repository.find_all(partition_key_value=self.partition_key_value,
event_ctx = self.create_event_context("read-many")
return self.repository.find_all(event_context=event_ctx,
conditions=conditions)

def get(self, id):
return self.repository.find(id, partition_key_value=self.partition_key_value)
event_ctx = self.create_event_context("read")
return self.repository.find(id, event_context=event_ctx)

def create(self, data: dict):
data[self.repository.partition_key_attribute] = self.partition_key_value
return self.repository.create(data)
event_ctx = self.create_event_context("create")
return self.repository.create(data, event_context=event_ctx)

def update(self, id, data: dict):
return self.repository.partial_update(id,
changes=data,
partition_key_value=self.partition_key_value)
event_ctx = self.create_event_context("update")
return self.repository.partial_update(id, changes=data, event_context=event_ctx)

def delete(self, id):
self.repository.delete(id, partition_key_value=self.partition_key_value)
event_ctx = self.create_event_context("update")
self.repository.delete(id, event_context=event_ctx)

@property
def partition_key_value(self):
return current_user_tenant_id()
def find_partition_key_value(self, event_context: EventContext):
return event_context.tenant_id

# Replace by decorator and put it in the repository
def create_event_context(self, action: str = None, description: str = None):
return EventContext(self.repository.container.id,
action,
description=description)


class CustomError(HTTPException):
Expand Down
29 changes: 29 additions & 0 deletions commons/data_access_layer/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

from flask import Flask

from time_tracker_api.security import current_user_id, current_user_tenant_id

COMMENTS_MAX_LENGTH = 500
ID_MAX_LENGTH = 64

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


class EventContext:
def __init__(self, container_id: str, action: str, description: str = None,
user_id: str = None, tenant_id: str = None, session_id: str = None):
self.container_id = container_id
self.action = action
self.description = description
self._user_id = user_id
self._tenant_id = tenant_id
self._session_id = session_id

@property
def user_id(self) -> str:
if self._user_id is None:
self._user_id = current_user_id()
return self._user_id

@property
def tenant_id(self) -> str:
if self._tenant_id is None:
self._tenant_id = current_user_tenant_id()
return self._tenant_id

@property
def session_id(self) -> str:
return self._session_id


def init_app(app: Flask) -> None:
init_cosmos_db(app)

Expand Down
6 changes: 6 additions & 0 deletions commons/data_access_layer/events_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from azure.cosmos import PartitionKey

container_definition = {
'id': 'event',
'partition_key': PartitionKey(path='/tenant_id')
}
19 changes: 19 additions & 0 deletions migrations/02-add-events.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
def up():
from commons.data_access_layer.cosmos_db import cosmos_helper
import azure.cosmos.exceptions as exceptions
from commons.data_access_layer.events_model import container_definition as event_definition
from . import app

app.logger.info("Creating container events...")

try:
app.logger.info('- Event')
cosmos_helper.create_container(event_definition)
except exceptions.CosmosResourceExistsError as e:
app.logger.warning("Unexpected error while creating container for events: %s" % e.message)

app.logger.info("Done!")


def down():
print("Not implemented!")
5 changes: 5 additions & 0 deletions requirements/time_tracker_events/dev.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# requirements/time_tracker_events/dev.txt


# Include the prod resources
-r prod.txt
5 changes: 5 additions & 0 deletions requirements/time_tracker_events/prod.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# requirements/time_tracker_events/prod.txt

# Azure Functions library
azure-functions

1 change: 1 addition & 0 deletions time_tracker_events/.funcignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

43 changes: 43 additions & 0 deletions time_tracker_events/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
bin
obj
csx
.vs
edge
Publish

*.user
*.suo
*.cscfg
*.Cache
project.lock.json

/packages
/TestResults

/tools/NuGet.exe
/App_Data
/secrets
/data
.secrets
appsettings.json
local.settings.json

node_modules
dist

# Local python packages
.python_packages/

# Python Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
34 changes: 34 additions & 0 deletions time_tracker_events/handle_events_trigger/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import logging
import uuid
from datetime import datetime

import azure.functions as func


def main(documents: func.DocumentList, events: func.Out[func.Document]):
if documents:
new_events = func.DocumentList()

for doc in documents:
logging.info(doc.to_json())

event_context = doc.get("_last_event_ctx")
if event_context is not None:
new_events.append(func.Document.from_dict({
"id": str(uuid.uuid4()),
"date": datetime.utcnow().isoformat(),
"user_id": event_context.get("user_id"),
"action": event_context.get("action"),
"description": event_context.get("description"),
"item_id": doc.get("id"),
"container_id": event_context.get("container_id"),
"session_id": event_context.get("session_id"),
"tenant_id": event_context.get("tenant_id"),
}))
else:
logging.warning("- Not saved!")

if len(new_events):
events.set(new_events)
else:
logging.warning("No valid events were found!")
26 changes: 26 additions & 0 deletions time_tracker_events/handle_events_trigger/function.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"scriptFile": "__init__.py",
"bindings": [
{
"type": "cosmosDBTrigger",
"name": "documents",
"direction": "in",
"leaseCollectionName": "leases",
"connectionStringSetting": "COSMOS_DATABASE_URI",
"databaseName": "time-tracker-db",
"collectionName": "activity",
"createLeaseCollectionIfNotExists": "true"
},
{
"direction": "out",
"type": "cosmosDB",
"name": "events",
"databaseName": "time-tracker-db",
"collectionName": "event",
"leaseCollectionName": "leases",
"createLeaseCollectionIfNotExists": true,
"connectionStringSetting": "COSMOS_DATABASE_URI",
"createIfNotExists": true
}
]
}
7 changes: 7 additions & 0 deletions time_tracker_events/host.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"version": "2.0",
"extensionBundle": {
"id": "Microsoft.Azure.Functions.ExtensionBundle",
"version": "[1.*, 2.0.0)"
}
}
8 changes: 8 additions & 0 deletions time_tracker_events/local.settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"IsEncrypted": false,
"Values": {
"FUNCTIONS_WORKER_RUNTIME": "python",
"AzureWebJobsStorage": "{AzureWebJobsStorage}",
"COSMOS_DATABASE_URI": "AccountEndpoint=https://time-tracker-db.documents.azure.com:443/;AccountKey=6lyRD8ma0VQjcMbeSMFOTDGwNDptcEGfngp3c9DStNAMCNh2MRbkNWmRinoNvuB6aH51EMEkgeP5WfW3VZiV9g==;"
}
}
4 changes: 4 additions & 0 deletions time_tracker_events/run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/usr/bin/env bash

echo Running Azure Functions locally....
func host start