Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
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
Binary file added .DS_Store
Binary file not shown.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,6 @@ swagger.json

# Local migration files
migration_status.csv

# Mac
.DS_Store
81 changes: 50 additions & 31 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,36 @@ 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)
@staticmethod
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 +191,31 @@ 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, mapper=mapper)

def update(self, id: str, item_data: dict, mapper: Callable = None):
self.on_update(item_data)
def update(self, id: str, item_data: dict, event_context: EventContext,
mapper: Callable = None):
self.on_update(item_data, event_context)
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):
return self.partial_update(id, {
'deleted': str(uuid.uuid4())
}, partition_key_value, peeker=peeker, visible_only=True, mapper=mapper)
'deleted': generate_uuid4()
}, event_context, 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,13 +224,15 @@ 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):
def on_update(self, update_item_data: dict, event_context: EventContext):
pass

def create_sql_order_clause(self):
Expand All @@ -228,27 +246,28 @@ 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,
conditions=conditions)
event_ctx = self.create_event_context("read-many")
return self.repository.find_all(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_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_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, data, event_ctx)

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

@property
def partition_key_value(self):
return current_user_tenant_id()
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
39 changes: 29 additions & 10 deletions commons/data_access_layer/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
"""
import abc

from flask import Flask

COMMENTS_MAX_LENGTH = 500
ID_MAX_LENGTH = 64

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


def init_app(app: Flask) -> None:
init_cosmos_db(app)
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 container_id(self):
return self._container_id

@property
def action(self):
return self._action

@property
def description(self):
return self._description

def init_sql(app: Flask) -> None:
from commons.data_access_layer.sql import init_app
init_app(app)
@property
def user_id(self):
return self._user_id

@property
def tenant_id(self):
return self._tenant_id

def init_cosmos_db(app: Flask) -> None:
from commons.data_access_layer.cosmos_db import init_app
init_app(app)
@property
def session_id(self):
return self._session_id
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')
}
4 changes: 1 addition & 3 deletions commons/data_access_layer/sql.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
from datetime import datetime

from flask import Flask
from flask_sqlalchemy import SQLAlchemy

from commons.data_access_layer.database import CRUDDao, ID_MAX_LENGTH
from commons.data_access_layer.database import CRUDDao

db: SQLAlchemy = None

Expand Down
2 changes: 0 additions & 2 deletions commons/git_hooks/enforce_semantic_commit_msg.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,10 @@

COMMIT_MSG_REGEX = r'(build|ci|docs|feat|fix|perf|refactor|style|test|chore|revert)(\([\w\-]+\))?:\s.*'


# Get the commit message file
commit_msg_file = open(sys.argv[1]) # The first argument is the file
commit_msg = commit_msg_file.read()


if re.match(COMMIT_MSG_REGEX, commit_msg) is None:
print(ERROR_MSG)
sys.exit(1)
Expand Down
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

3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from setuptools import setup, find_packages
import sys

from setuptools import setup, find_packages


def get_version() -> str:
version = {}
Expand Down
Loading