Skip to content

Commit

Permalink
Merge pull request #19 from bsa7/SMLP-018-implement-candles-model
Browse files Browse the repository at this point in the history
SMLP-018 Implement Candles model
  • Loading branch information
bsa7 authored Mar 11, 2023
2 parents 338fde8 + 8d4bd3b commit 9ab8e34
Show file tree
Hide file tree
Showing 19 changed files with 192 additions and 51 deletions.
1 change: 1 addition & 0 deletions .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ disable=bad-inline-option,
no-name-in-module,
raw-checker-failed,
suppressed-message,
too-few-public-methods,
use-symbolic-message-instead,
useless-suppression,
wrong-import-order
Expand Down
5 changes: 5 additions & 0 deletions .test.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
API_EXMO_HOST=https://api.exmo.com/v1.1
MONGO_USER_NAME=
MONGO_USER_PASSWORD=
MONGO_DATABASE_NAME=smlp
MONGO_HOST=localhost:45845
19 changes: 0 additions & 19 deletions app/lib/db_client.py

This file was deleted.

5 changes: 5 additions & 0 deletions app/lib/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,8 @@ def __init__(self):
def get(self, variable_name: str):
''' This metod reads environment variable value '''
return os.getenv(variable_name)

@property
def name(self) -> str:
''' Returns environment name (test | development | production) '''
return os.getenv('PYTHON_ENV')
35 changes: 22 additions & 13 deletions app/lib/mongo_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,36 +5,45 @@

class MongoClient(metaclass = Singleton):
''' This class implements mongo db client '''
def __init__(self, connection_url = None):
url = connection_url or self.__connection_string
self.client = pymongo.MongoClient(url)
self.database = self.client[self.__mongo_database_name()]
def __init__(self):
print(f'{self.__connection_string=}')
self.client = pymongo.MongoClient(self.__connection_string)
self.database = self.client[self.__mongo_database_name]
self.collection = self.database['collection']

def check_connection(self):
''' Checks mongodb connection '''
return self.client.server_info()['version'] is not None

def collection(self, collection_name: str):
''' Returns collection by its name '''
return self.database[collection_name]

@property
def __connection_string(self):
''' Returns connection string '''
return f"mongodb://{self.__mongo_user_name()}:{self.__mongo_password()}@{self.__mongo_host()}"
return f"mongodb://{self.__auth_string}{self.__mongo_host}"

@property
def __auth_string(self) -> str:
''' Returns authentication string '''
if self.__mongo_user_name is None or self.__mongo_user_name == '':
return ''

return f"{self.__mongo_user_name}:{self.__mongo_password}@"

def __mongo_user_name(self):
@property
def __mongo_user_name(self) -> str:
''' Returns mongodb user name '''
return Env().get('MONGO_USER_NAME')

def __mongo_password(self):
@property
def __mongo_password(self) -> str:
''' Returns mongodb user passord '''
return Env().get('MONGO_USER_PASSWORD')

def __mongo_database_name(self):
@property
def __mongo_database_name(self) -> str:
''' Returns mongodb database name '''
return Env().get('MONGO_DATABASE_NAME')

def __mongo_host(self):
@property
def __mongo_host(self) -> str:
''' Returns mongodb host '''
return Env().get('MONGO_HOST')
24 changes: 24 additions & 0 deletions app/lib/service_factory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
''' This file contains implementation of ServiceFactory class '''
from app.lib.singleton import Singleton
from app.lib.env import Env
from app.lib.mongo_client import MongoClient
from app.lib.test.mongo_client import MongoClient as TestMongoClient

class ServiceFactory(metaclass = Singleton):
''' This class produces client classes for various services '''
@property
def mongo_client(self):
''' Returns class for mongo_client '''
return self.__client_by_env_name(production = MongoClient, test = TestMongoClient)

def __client_by_env_name(self, development = None, production = None, test = None):
''' Returns one from given attributes depending on env name '''
env_name = Env().name
print(f'{env_name=}')
if env_name == 'test':
return test or development or production

if env_name == 'development':
return development or production

return production or development
38 changes: 38 additions & 0 deletions app/lib/test/mongo_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
''' This file contains self-writed implementation for inmemory mongo client '''

class Collection:
''' This class contains emulation of mongodb collection '''
__storage = []

@classmethod
def find_many(cls, filter_attributes):
''' Returns list of filtered documents '''
def filter_lambda(item):
return item.items() | filter_attributes.items() == item.items()

result = filter(filter_lambda, cls.__storage)
return list(result)

@classmethod
def count_documents(cls, filter_attributes) -> int:
''' Returns count of filtered documents '''
return len(cls.find_many(filter_attributes))

@classmethod
def insert_one(cls, record_attributes):
''' Insert one record to storage '''
return cls.__storage.append(record_attributes)

@classmethod
def find_one(cls, filter_attributes):
''' Find first record with exact attributes '''
return cls.find_many(filter_attributes)[0]

@classmethod
def cleanup(cls):
''' Clears internal storage '''
cls.__storage = []

class MongoClient:
''' This class implements mongo db client '''
collection = Collection
34 changes: 34 additions & 0 deletions app/models/application_record.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
''' This file contains ApplicationRecord abstract class definition '''
from app.lib.service_factory import ServiceFactory

class ApplicationRecord():
''' This abstract class contains base methods for data operation in mongo db '''
collection = ServiceFactory().mongo_client().collection

def __init__(self):
''' Initializes instance '''

@classmethod
def count(cls, **filter_attributes) -> int:
''' Returns count of model's records '''
return cls.collection.count_documents({ 'model': cls.__name__, **filter_attributes })

@classmethod
def find_one(cls, **attributes):
''' find record by filter '''
return cls.collection.find_one({ 'model': cls.__name__, **attributes })

@classmethod
def insert_one(cls, **record_attributes):
''' Insert one record model's records '''
return cls.collection.insert_one({ 'model': cls.__name__, **record_attributes })

@classmethod
def upsert_one(cls, find_by, data):
''' Update existed or create new record '''
existed_item = cls.find_one(**find_by)
if existed_item is None:
return cls.insert_one(**find_by, **data)

print(f'{find_by=}, {data=}')
return cls.collection.update_one({ 'model': cls.__name__, **find_by }, { '$set': data })
5 changes: 5 additions & 0 deletions app/models/candle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
''' This file contains Candle model class definition '''
from app.models.application_record import ApplicationRecord

class Candle(ApplicationRecord):
'''This class contains Candle class definition'''
9 changes: 7 additions & 2 deletions data_creation.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,18 @@
from app.lib.api_exmo_client import ApiExmoClient
from app.lib.data_fetcher import DataFetcher
from app.lib.utils import current_timestamp, days
from app.models.candle import Candle

from_timestamp = current_timestamp() - days(365)
data_fetcher = DataFetcher(api_client = ApiExmoClient(resolution = 'D'),
from_timestamp = from_timestamp,
to_timestamp = current_timestamp(),
batch_size_in_milliseconds = days(25))

result = data_fetcher.update('BTC_USDT')
INSTRUMENT_NAME = 'BTC_USDT'
result = data_fetcher.update(INSTRUMENT_NAME)

print(f'{result=}')
for item in result:
timestamp = item['t']
average_value = (item['o'] + item['c']) / 2
Candle.upsert_one(find_by = { 'ds': timestamp, 'instrument': INSTRUMENT_NAME }, data = { 'y': average_value })
6 changes: 6 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[pytest]
env_files =
.env
.test.env
testpaths =
test
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
mockupdb==1.8.1
pylint==2.16.2
pymongo==4.3.3
pytest-dotenv==0.5.2
pytest==7.2.1
python-dotenv==1.0.0
requests==2.28.2
Expand Down
2 changes: 1 addition & 1 deletion scripts/run_pytest
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
#!/usr/bin/env bash

python -m pytest test --capture=sys
PYTHON_ENV=test python -m pytest --capture=sys --envfile=.test.env
2 changes: 1 addition & 1 deletion test/lib/api_exmo_client_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import responses
from app.types.api_exmo_responses import CandlesHistory
from app.lib.api_exmo_client import ApiExmoClient
from test.support.mock_helper import stub_get_request
from test.support.stub_helper import stub_get_request

class TestApiExmoClient(unittest.TestCase):
''' This class contains tests for ApiExmoClient class '''
Expand Down
2 changes: 1 addition & 1 deletion test/lib/data_fetcher_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from app.lib.api_exmo_client import ApiExmoClient
from app.lib.utils import days, seconds
from app.types.api_exmo_responses import CandlesHistory
from test.support.mock_helper import stub_get_request
from test.support.stub_helper import stub_get_request

class TestDataFetcher(unittest.TestCase):
''' This class contains tests for DataFetcher class '''
Expand Down
5 changes: 5 additions & 0 deletions test/lib/env_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,8 @@ def test_get_unexisted_environment_variable(self):
expected_value = None
var_name = 'TEST_VARIABLE'
self.assertEqual(Env().get(var_name), expected_value)

def test_get_env_name(self):
''' This case checks if Env.name returns right test environment name '''
expected_env_name = 'test'
self.assertEqual(Env().name, expected_env_name)
18 changes: 5 additions & 13 deletions test/lib/mongo_client_test.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,18 @@
''' This file contains unit tests for mongo client class instance '''
import unittest
from mockupdb import going, MockupDB
from app.lib.mongo_client import MongoClient
from app.lib.service_factory import ServiceFactory

class TestMongoClient(unittest.TestCase):
''' This class contains unit tests for mongo client '''
def setUp(self):
''' Initializes mock server for mongodb client '''
self.__server = MockupDB(auto_ismaster = True)
self.__server.autoresponds('ismaster', maxWireVersion = 6)
self.__server.run()
self.addCleanup(self.__server.stop)
self.__client = MongoClient(self.__server.uri)
self.__collection = ServiceFactory().mongo_client().collection

def test_insert_one(self):
''' This case checks if the mongo client inserts one record to collection '''
expected_document = { '_id': 'id-100500' }
with going(self.__client.database.collection.insert_one, expected_document) as future:
self.__server.receives().ok()

result = future()
self.assertEqual('id-100500', result.inserted_id)
self.__collection.insert_one(expected_document)
result = self.__collection.find_one(expected_document)
self.assertEqual(expected_document, result)

def test_read_collection_size(self):
''' This case checks the mongo client receive correct size of collection '''
31 changes: 31 additions & 0 deletions test/models/application_record_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
''' This file contains test for Candle model tests '''
import unittest
from app.models.application_record import ApplicationRecord
from app.models.candle import Candle

class TestApplicationRecord(unittest.TestCase):
''' This class contains tests for base methods of Candle::ApplicationRecord model class '''

def tearDown(self):
''' Clean up after each test '''
ApplicationRecord.collection.cleanup()

def test_zero_count(self):
''' This case checks if model can get correct zero count when no record exists '''
count = Candle.count()
self.assertEqual(count, 0)

def test_count(self):
''' This case checks if model can get correct zero count when some records are exists '''
Candle.insert_one(ds = 123456, y = 123)
Candle.insert_one(ds = 123457, y = 124)
self.assertEqual(Candle.count(), 2)

def test_insert_one(self):
''' This case checks if model correctly insert one record '''
record_search_attributes = { 'ds': 12345 }
record_attributes = { 'y': 123 }
expected_record_attributes = { 'model': 'Candle', **record_search_attributes, **record_attributes }
Candle.insert_one(**record_search_attributes, **record_attributes)
created_record = Candle.find_one(**record_search_attributes)
self.assertEqual(created_record, expected_record_attributes)
File renamed without changes.

0 comments on commit 9ab8e34

Please sign in to comment.