Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ automatically [pip](https://pip.pypa.io/en/stable/) as well.
python3 -m pip install -r requirements/<app>/<stage>.txt
```

Where <app> is one of the executable app namespace, e.g. `time_tracker_api` or `time_tracker_events`.
Where `<app>` is one of the executable app namespace, e.g. `time_tracker_api` or `time_tracker_events`.
The `stage` can be

* `dev`: Used for working locally
Expand Down
62 changes: 56 additions & 6 deletions commons/data_access_layer/cosmos_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import azure.cosmos.cosmos_client as cosmos_client
import azure.cosmos.exceptions as exceptions
import flask
from azure.cosmos import ContainerProxy, PartitionKey
from flask import Flask
from werkzeug.exceptions import HTTPException
Expand Down Expand Up @@ -101,8 +102,8 @@ def __init__(
raise ValueError("The cosmos_db module has not been initialized!")
self.mapper = mapper
self.order_fields = order_fields if order_fields else []
self.container: ContainerProxy = self.cosmos_helper.db.get_container_client(
container_id
self.container: ContainerProxy = (
self.cosmos_helper.db.get_container_client(container_id)
)
self.partition_key_attribute = partition_key_attribute

Expand Down Expand Up @@ -266,7 +267,6 @@ def find_all(
),
order_clause=self.create_sql_order_clause(),
)

result = self.container.query_items(
query=query_str,
parameters=params,
Expand All @@ -277,6 +277,50 @@ def find_all(
function_mapper = self.get_mapper_or_dict(mapper)
return list(map(function_mapper, result))

def count(
self,
event_context: EventContext,
conditions: dict = None,
custom_sql_conditions: List[str] = None,
custom_params: dict = None,
visible_only=True,
):
conditions = conditions if conditions else {}
custom_sql_conditions = (
custom_sql_conditions if custom_sql_conditions else []
)
custom_params = custom_params if custom_params else {}
partition_key_value = self.find_partition_key_value(event_context)
params = [
{"name": "@partition_key_value", "value": partition_key_value},
]
params.extend(self.generate_params(conditions))
params.extend(custom_params)
query_str = """
SELECT VALUE COUNT(1) FROM c
WHERE c.{partition_key_attribute}=@partition_key_value
{conditions_clause}
{visibility_condition}
{custom_sql_conditions_clause}
""".format(
partition_key_attribute=self.partition_key_attribute,
visibility_condition=self.create_sql_condition_for_visibility(
visible_only
),
conditions_clause=self.create_sql_where_conditions(conditions),
custom_sql_conditions_clause=self.create_custom_sql_conditions(
custom_sql_conditions
),
)

flask.current_app.logger.debug(query_str)
result = self.container.query_items(
query=query_str,
parameters=params,
partition_key=partition_key_value,
)
return result.next()

def partial_update(
self,
id: str,
Expand All @@ -286,7 +330,10 @@ def partial_update(
mapper: Callable = None,
):
item_data = self.find(
id, event_context, visible_only=visible_only, mapper=dict,
id,
event_context,
visible_only=visible_only,
mapper=dict,
)
item_data.update(changes)
return self.update(id, item_data, event_context, mapper=mapper)
Expand All @@ -304,7 +351,10 @@ def update(
return function_mapper(self.container.replace_item(id, body=item_data))

def delete(
self, id: str, event_context: EventContext, mapper: Callable = None,
self,
id: str,
event_context: EventContext,
mapper: Callable = None,
):
return self.partial_update(
id,
Expand All @@ -327,7 +377,7 @@ def get_mapper_or_dict(self, alternative_mapper: Callable) -> Callable:
def get_page_size_or(self, custom_page_size: int) -> int:
# TODO The default value should be taken from the Azure Feature Manager
# or any other repository for the settings
return custom_page_size or 100
return custom_page_size or 9999

def on_update(self, update_item_data: dict, event_context: EventContext):
pass
Expand Down
27 changes: 23 additions & 4 deletions tests/commons/data_access_layer/cosmos_db_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,10 @@ def test_create_with_diff_unique_data_but_same_tenant_should_succeed(
):
new_data = sample_item.copy()
new_data.update(
{'id': fake.uuid4(), 'email': fake.safe_email(),}
{
'id': fake.uuid4(),
'email': fake.safe_email(),
}
)

result = cosmos_db_repository.create(new_data, event_context)
Expand All @@ -97,7 +100,9 @@ def test_create_with_same_id_should_fail(
try:
new_data = sample_item.copy()
new_data.update(
{'email': fake.safe_email(),}
{
'email': fake.safe_email(),
}
)

cosmos_db_repository.create(new_data, event_context)
Expand Down Expand Up @@ -134,7 +139,9 @@ def test_create_with_same_id_but_diff_partition_key_attrib_should_succeed(
new_data = sample_item.copy()

new_data.update(
{'tenant_id': another_tenant_id,}
{
'tenant_id': another_tenant_id,
}
)

result = cosmos_db_repository.create(new_data, another_event_context)
Expand Down Expand Up @@ -310,6 +317,15 @@ def test_find_all_with_offset(
assert result_after_the_second_item == result_all_items[2:]


def test_count(
cosmos_db_repository: CosmosDBRepository, event_context: EventContext
):
counter = cosmos_db_repository.count(event_context)
print('test counter: ', counter)

assert counter == 10


@pytest.mark.parametrize(
'mapper,expected_type', [(None, dict), (dict, dict), (Person, Person)]
)
Expand Down Expand Up @@ -405,7 +421,10 @@ def test_update_with_mapper(
):
changed_item = sample_item.copy()
changed_item.update(
{'name': fake.name(), 'email': fake.safe_email(),}
{
'name': fake.name(),
'email': fake.safe_email(),
}
)

updated_item = cosmos_db_repository.update(
Expand Down
18 changes: 12 additions & 6 deletions tests/time_tracker_api/time_entries/time_entries_namespace_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -730,26 +730,31 @@ def test_summary_is_called_with_date_range_from_worked_time_module(


def test_paginated_fails_with_no_params(
client: FlaskClient, valid_header: dict,
client: FlaskClient,
valid_header: dict,
):
response = client.get('/time-entries/paginated', headers=valid_header)
assert HTTPStatus.BAD_REQUEST == response.status_code


def test_paginated_succeeds_with_valid_params(
client: FlaskClient, valid_header: dict,
client: FlaskClient,
valid_header: dict,
):
response = client.get(
'/time-entries/paginated?start=10&length=10', headers=valid_header
'/time-entries/paginated?start_date=2020-09-10T00:00:00-05:00&end_date=2020-09-10T23:59:59-05:00&timezone_offset=300&start=0&length=5',
headers=valid_header,
)
assert HTTPStatus.OK == response.status_code


def test_paginated_response_contains_expected_props(
client: FlaskClient, valid_header: dict,
client: FlaskClient,
valid_header: dict,
):
response = client.get(
'/time-entries/paginated?start=10&length=10', headers=valid_header
'/time-entries/paginated?start_date=2020-09-10T00:00:00-05:00&end_date=2020-09-10T23:59:59-05:00&timezone_offset=300&start=0&length=5',
headers=valid_header,
)
assert 'data' in json.loads(response.data)
assert 'records_total' in json.loads(response.data)
Expand All @@ -761,7 +766,8 @@ def test_paginated_sends_max_count_and_offset_on_call_to_repository(
time_entries_dao.repository.find_all = Mock(return_value=[])

response = client.get(
'/time-entries/paginated?start=10&length=10', headers=valid_header
'/time-entries/paginated?start_date=2020-09-10T00:00:00-05:00&end_date=2020-09-10T23:59:59-05:00&timezone_offset=300&start=0&length=5',
headers=valid_header,
)

time_entries_dao.repository.find_all.assert_called_once()
Expand Down
14 changes: 10 additions & 4 deletions time_tracker_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
flask_app: Flask = None


def create_app(config_path='time_tracker_api.config.DefaultConfig',
config_data=None):
def create_app(
config_path='time_tracker_api.config.DefaultConfig', config_data=None
):
global flask_app
flask_app = Flask(__name__)

Expand Down Expand Up @@ -36,13 +37,15 @@ def init_app_config(app: Flask, config_path: str, config_data: dict = None):

def init_app(app: Flask):
from time_tracker_api.database import init_app as init_database

init_database(app)

from time_tracker_api.api import init_app

init_app(app)

if app.config.get('DEBUG'):
app.logger.setLevel(logging.INFO)
app.logger.setLevel(logging.DEBUG)
add_debug_toolbar(app)

add_werkzeug_proxy_fix(app)
Expand All @@ -62,22 +65,25 @@ def add_debug_toolbar(app: Flask):
'flask_debugtoolbar.panels.template.TemplateDebugPanel',
'flask_debugtoolbar.panels.logger.LoggingPanel',
'flask_debugtoolbar.panels.route_list.RouteListDebugPanel',
'flask_debugtoolbar.panels.profiler.ProfilerDebugPanel'
'flask_debugtoolbar.panels.profiler.ProfilerDebugPanel',
)

from flask_debugtoolbar import DebugToolbarExtension

toolbar = DebugToolbarExtension()
toolbar.init_app(app)


def enable_cors(app: Flask, cors_origins: str):
from flask_cors import CORS

cors_origins_list = cors_origins.split(",")
CORS(app, resources={r"/*": {"origins": cors_origins_list}})
app.logger.info("Set CORS access to [%s]" % cors_origins)


def add_werkzeug_proxy_fix(app: Flask):
from werkzeug.middleware.proxy_fix import ProxyFix

app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1)
app.logger.info("Add ProxyFix to serve swagger.json over https.")
5 changes: 5 additions & 0 deletions time_tracker_api/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,17 +126,20 @@ def init_app(app: Flask):

@api.errorhandler(CosmosResourceExistsError)
def handle_cosmos_resource_exists_error(error):
app.logger.error(error)
return {'message': 'It already exists'}, HTTPStatus.CONFLICT


@api.errorhandler(CosmosResourceNotFoundError)
@api.errorhandler(StopIteration)
def handle_not_found_errors(error):
app.logger.error(error)
return {'message': 'It was not found'}, HTTPStatus.NOT_FOUND


@api.errorhandler(CosmosHttpResponseError)
def handle_cosmos_http_response_error(error):
app.logger.error(error)
return (
{'message': 'Invalid request. Please verify your data.'},
HTTPStatus.BAD_REQUEST,
Expand All @@ -145,6 +148,7 @@ def handle_cosmos_http_response_error(error):

@api.errorhandler(AttributeError)
def handle_attribute_error(error):
app.logger.error(error)
return (
{'message': "There are missing attributes"},
HTTPStatus.UNPROCESSABLE_ENTITY,
Expand All @@ -153,6 +157,7 @@ def handle_attribute_error(error):

@api.errorhandler(CustomError)
def handle_custom_error(error):
app.logger.error(error)
return {'message': error.description}, error.code


Expand Down
Loading