Coverage for /home/ubuntu/shekels/python/shekels/core/database.py: 100%
49 statements
« prev ^ index » next coverage.py v7.1.0, created at 2023-11-15 00:54 +0000
« prev ^ index » next coverage.py v7.1.0, created at 2023-11-15 00:54 +0000
1from typing import List, Union # noqa: F401
2from pathlib import Path # noqa: F401
4from copy import deepcopy
5from functools import lru_cache
7import jsoncomment as jsonc
8import numpy as np
9import pandas as pd
11from shekels.core.config import Config
12import shekels.core.data_tools as sdt
13# ------------------------------------------------------------------------------
16class Database:
17 '''
18 Database is a class for wrapping a mint transaction DataFrame with a simple
19 CRUD-like API. API methods include: update, read and search.
20 '''
21 @staticmethod
22 def from_json(filepath):
23 # type: (Union[str, Path]) -> Database
24 '''
25 Constructs a Database instance from a given JSON filepath.
27 Args:
28 filepath(Path or str): Path to JSON config file.
30 Returns:
31 Database: Database instance.
32 '''
33 with open(filepath) as f:
34 config = jsonc.JsonComment().load(f)
35 return Database(config)
37 def __init__(self, config):
38 # type: (dict) -> None
39 '''
40 Constructs a Database instance.
42 Args:
43 config (dict): Configuration.
44 '''
45 config = Config(config)
46 config.validate()
47 self._config = config.to_primitive()
48 self._data = None # type: Union[None, pd.DataFrame]
50 @staticmethod
51 def _to_records(data):
52 # type: (pd.DataFrame) -> List[dict]
53 '''
54 Converts given DataFrame to a list of JSONifiable dicts.
56 Args:
57 data (DataFrame): Data.
59 Returns:
60 list[dict]: Records.
61 '''
62 data = data.copy()
63 data.date = data.date.apply(lambda x: x.isoformat())
64 data = data.replace({np.nan: None}).to_dict(orient='records')
65 return data
67 @property
68 def config(self):
69 # type: () -> dict
70 '''
71 Returns a copy of this instance's configuration.
73 Returns:
74 dict: Copy of config.
75 '''
76 return deepcopy(self._config)
78 @property
79 def data(self):
80 # type: () -> Union[None, pd.DataFrame]
81 '''
82 Returns a copy of this instance's data.
84 Returns:
85 DataFrame: Copy of data.
86 '''
87 if self._data is None:
88 return None
89 return self._data.copy()
91 def update(self):
92 # type: () -> Database
93 '''
94 Loads CSV found in config's data_path into self._data.
96 Returns:
97 Database: self.
98 '''
99 data = pd.read_csv(self._config['data_path'], index_col=None)
100 self._data = sdt.conform(
101 data,
102 actions=self._config['conform'],
103 columns=self._config['columns'],
104 )
105 return self
107 @lru_cache(maxsize=1)
108 def read(self):
109 # type: () -> List[dict]
110 '''
111 Returns data if update has been called.
113 Raises:
114 RuntimeError: If update has not first been called.
116 Returns:
117 list[dict]: Data as records.
118 '''
119 if self._data is None:
120 msg = 'Database not updated. Please call update.'
121 raise RuntimeError(msg)
122 return self._to_records(self._data)
124 @lru_cache()
125 def search(self, query):
126 # type: (str) -> List[dict]
127 '''
128 Search data according to given SQL query.
130 Args:
131 query (str): SQL query. Make sure to use "FROM data" in query.
133 Returns:
134 DataFrame: Formatted data.
135 '''
136 output = sdt.query_data(self._data, query)
137 # pandasql coerces Timestamps to strings
138 output.date = pd.DatetimeIndex(output.date)
139 return self._to_records(output)