Skip to content

Commit 4853c99

Browse files
authored
version: 0.2.4 (#11)
* change!(db.find): pass key, value into lambda function * feat: create `Condition` to find method * feat(Condition): supports `AND`, `OR` * change!(db.find): restore not to pass object id as key * fix(Condition): compare with None * test(db.find): add unit tests * change exporting module defines * docs: update `Database.find()` * refactor: pull method up into interface * refactor: move method into util module * refactor: reorganize module directories * feat(Database): beautify representation * feat: add decorater to document * fix: add missing dict method * tests: add unit tests for dictionary methods * tests: add `fail()` method * docs: update changes * refactor(tests): rename methods * fix(database): getter with list * docs: update changes and example * feat: add static method `load` (#7) * docs: update changes * fix: change `__version__` to constant value * version up to 0.2.4 * docs: fix wrong urls * build: add more trigger types
2 parents 0c13c7c + 9da148d commit 4853c99

File tree

19 files changed

+392
-68
lines changed

19 files changed

+392
-68
lines changed

.github/workflows/pytest.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,13 @@ on:
44
push:
55
branches:
66
- 'main'
7+
- 'dev'
78
pull_request:
89
types:
910
- opened
11+
- edited
12+
- reopened
13+
- review_requested
1014
- ready_for_review
1115
branches:
1216
- 'main'

CHANGELOG.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12-
- Supports to find by key and value with `Condition` that is generated from `Key`.
12+
- Supports to find by key and value with `Condition` that is generated from `Key`. [(#8)](https://github.com/joonas-yoon/json-as-db/issues/8)
13+
- Implements useful representation for quickly displaying as a table format. [(#4)](https://github.com/joonas-yoon/json-as-db/issues/4)
14+
- Add static method to load - `json_as_db.load(path)` [(#7)](https://github.com/joonas-yoon/json-as-db/issues/7)
15+
- Add a variable `__version__` in global.
16+
17+
### Fixed
18+
19+
- Implements `items()` methods to override dictionary on `Database` class. [(#3)](https://github.com/joonas-yoon/json-as-db/issues/3)
20+
- Getter supports list-like parameter such as `db[['id1', 'id2']]` [(#5)](https://github.com/joonas-yoon/json-as-db/issues/5)
1321

1422
Thanks for getting this release out! please stay tuned :)
1523

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "json_as_db"
3-
version = "0.2.3"
3+
version = "0.2.4"
44
description = "Using JSON as very lightweight database"
55
readme = "README.md"
66
license = "MIT"

src/json_as_db/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
from .core.database import Database
22
from .core.matcher import Condition, Conditions, Key
3-
4-
__version__ = '0.2.4'
3+
from .statics import load
4+
from .constants import __version__
55

66
__all__ = [
7+
"__version__",
78
"Condition",
89
"Conditions",
910
"Database",
1011
"Key",
12+
"load",
1113
]
File renamed without changes.

src/json_as_db/_utils/decorater.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from typing import Callable
2+
3+
4+
def copy_doc(original: Callable) -> Callable:
5+
def wrapper(target: Callable) -> Callable:
6+
target.__doc__ = original.__doc__
7+
return target
8+
return wrapper

src/json_as_db/constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
package_name = "json_as_db"
2+
3+
__version__ = '0.2.4'

src/json_as_db/core/_formatting.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
from typing import List, Tuple
2+
3+
4+
def split_first_last(l: list, k: int) -> Tuple[list, list]:
5+
_len = len(l)
6+
if _len <= 1:
7+
return l, []
8+
is_odd = k % 2
9+
k = min(k // 2, _len // 2)
10+
return l[:(k+int(is_odd))], l[-k:]
11+
12+
13+
def to_plural(unit_str: str, k: int) -> str:
14+
return unit_str + ('' if k < 2 else 's')
15+
16+
17+
def collapsed_row(l: list, is_skip: bool) -> list:
18+
_first, _last = split_first_last(l, len(l))
19+
return _first + (['...'] if is_skip else []) + _last
20+
21+
22+
def collapse_str(s: str, width: int) -> str:
23+
if len(s) <= width:
24+
return s
25+
return s[:min(len(s), width - 3)] + '...'
26+
27+
28+
def row_str(l: list, delimiter: str = '') -> str:
29+
return f" {delimiter} ".join(l) + "\n"
30+
31+
32+
def row_padded(row: list, widths: list) -> List[str]:
33+
return [row[i].ljust(widths[i]) for i in range(len(row))]
34+
35+
36+
def stringify(all_items: List[dict]) -> str:
37+
"""Return 3 each rows from the top and the bottom.
38+
39+
Returns:
40+
str: The first and last 3 rows of the caller object.
41+
42+
Example:
43+
>>> db
44+
age grouped ... job name
45+
32 True ... Camera operator Layne
46+
17 False ... Flying instructor Somerled
47+
9 True ... Inventor Joon-Ho
48+
... ... ... ... ...
49+
23 None ... Publican Melanie
50+
54 True ... Racing driver Eike
51+
41 None ... Barrister Tanja
52+
53+
54+
[100 items, 9 keys]
55+
"""
56+
# Collect key names to be column
57+
keys = set()
58+
for item in all_items:
59+
keys |= set(item.keys())
60+
total_rows = len(all_items)
61+
total_cols = len(keys)
62+
63+
# Display options
64+
keys = sorted(list(keys))
65+
rows_display = 6
66+
cols_display = 4
67+
text_max_width = 12
68+
69+
# Collect rows by columns and collapse them
70+
first_cols, last_cols = split_first_last(keys, cols_display)
71+
first_rows, last_rows = split_first_last(all_items, rows_display)
72+
rows = first_rows + last_rows
73+
cols = first_cols + last_cols
74+
col_widths = [max(3, len(str(col))) for col in cols]
75+
table = []
76+
for irow, row in enumerate(rows):
77+
t_row = []
78+
for icol, col in enumerate(cols):
79+
try:
80+
stringified = str(row[col])
81+
except KeyError:
82+
stringified = str(None)
83+
text = collapse_str(stringified, width=text_max_width)
84+
col_widths[icol] = max(col_widths[icol], len(text))
85+
t_row.append(text)
86+
table.append(t_row)
87+
88+
# Shortcut functions
89+
def _clp_row(l): return collapsed_row(l, is_skip=total_cols > cols_display)
90+
def _make_row_str(l): return row_str(_clp_row(l), delimiter='')
91+
def _padded(l): return row_padded(l, widths=col_widths)
92+
93+
# Create result strings
94+
result = _make_row_str(_padded(first_cols + last_cols))
95+
for irow, row in enumerate(table):
96+
s = _padded(row)
97+
result += _make_row_str(s)
98+
if total_rows > rows_display and irow + 1 == len(first_rows):
99+
result += _make_row_str(_padded(['...'] * (len(cols))))
100+
101+
result += "\n\n" + ", ".join([
102+
f"[{total_rows} {to_plural('item', total_rows)}",
103+
f"{total_cols} {to_plural('key', total_cols)}]",
104+
])
105+
106+
return result

src/json_as_db/core/database.py

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
from datetime import datetime
77
from typing import Any, Union, List, Callable
88

9-
from ..constants import package_name
10-
from .._utils import override_dict, from_maybe_list, return_maybe
9+
from ..constants import package_name, __version__
10+
from .._utils import override_dict, from_maybe_list, return_maybe, decorater
11+
from ._formatting import stringify
1112

1213
__all__ = [
1314
'Database'
@@ -16,7 +17,6 @@
1617

1718
class Database(dict):
1819
__data__ = 'data'
19-
__version__ = '1.0.0'
2020
__metadata__ = [
2121
'version',
2222
'creator',
@@ -34,18 +34,15 @@ def __init__(self, *arg, **kwargs) -> None:
3434
def _create_empty(self) -> dict:
3535
now = datetime.now().isoformat()
3636
return {
37-
'version': self.__version__,
37+
'version': __version__,
3838
'creator': package_name,
3939
'created_at': now,
4040
'updated_at': now,
4141
self.__data__: dict(),
4242
}
4343

44-
def __getitem__(self, key: str) -> Any:
45-
try:
46-
return self.data.__getitem__(key)
47-
except KeyError:
48-
return None
44+
def __getitem__(self, key: Union[str, List[str]]) -> Union[Any, List[Any]]:
45+
return self.get(key)
4946

5047
def __setitem__(self, key, value) -> None:
5148
raise NotImplementedError('Can not set attributes directly')
@@ -68,21 +65,30 @@ def __exports_only_publics(self) -> dict:
6865
out.pop(key)
6966
return out
7067

71-
def __repr__(self):
72-
return self.__exports_only_publics().__repr__()
68+
@decorater.copy_doc(stringify)
69+
def __repr__(self) -> str:
70+
return stringify(list(self.data.values()))
7371

74-
def __str__(self):
72+
@decorater.copy_doc(stringify)
73+
def __str__(self) -> str:
7574
return str(self.__repr__())
7675

77-
def __len__(self):
76+
def __len__(self) -> int:
7877
return len(self.data)
7978

8079
def keys(self) -> list:
8180
return self.data.keys()
8281

82+
def items(self) -> list:
83+
return self.data.items()
84+
8385
def values(self) -> list:
8486
return self.data.values()
8587

88+
@property
89+
def version(self) -> str:
90+
return self.__dict__.get('version')
91+
8692
@property
8793
def data(self) -> dict:
8894
return self.__dict__.get(self.__data__)
@@ -100,6 +106,24 @@ def _update_timestamp(self) -> None:
100106
})
101107

102108
def get(self, key: Union[str, List[str]], default=None) -> Union[Any, List[Any]]:
109+
"""Get objects by given IDs when list is given.
110+
When single string is given, returns single object by given key
111+
112+
Args:
113+
key (str | List[str]): single key or list-like
114+
default (Any, optional): default value if not exists. Defaults to None.
115+
116+
Returns:
117+
Any | List[Any]: single object or list-like
118+
119+
Examples:
120+
>>> db.get('kcbPuqpfV3YSHT8YbECjvh')
121+
{...}
122+
>>> db.get(['kcbPuqpfV3YSHT8YbECjvh'])
123+
[{...}]
124+
>>> db.get(['kcbPuqpfV3YSHT8YbECjvh', 'jmJKBJBAmGESC3rGbSb62T'])
125+
[{...}, {...}]
126+
"""
103127
_type, _keys = from_maybe_list(key)
104128
values = [self.data.get(k, default) for k in _keys]
105129
return return_maybe(_type, values)

src/json_as_db/statics.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from .core.database import Database
2+
3+
4+
def load(path: str, *args, **kwargs) -> Database:
5+
return Database().load(path, *args, **kwargs)
6+

tests/database/test_dict.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import os
2+
import pytest
3+
4+
from utils import file, logger, fail
5+
6+
from json_as_db import Database
7+
8+
9+
CUR_DIR = os.path.dirname(os.path.realpath(__file__))
10+
DB_FILENAME = 'db.json'
11+
DB_FILEPATH = os.path.join(CUR_DIR, '..', 'samples', DB_FILENAME)
12+
REC_ID = 'kcbPuqpfV3YSHT8YbECjvh'
13+
REC_ID_2 = 'jmJKBJBAmGESC3rGbSb62T'
14+
REC_ID_NOT_EXIST = 'N0t3xIstKeyV41ueString'
15+
DB_STR_OUTPUT = """booleanFalse booleanTrue ... randomInteger randomString
16+
False True ... 123 keyboard-cat
17+
None None ... 321 cheshire-cat
18+
19+
20+
[2 items, 6 keys]"""
21+
22+
23+
@pytest.fixture()
24+
def db() -> Database:
25+
return Database().load(DB_FILEPATH)
26+
27+
28+
def test_getter(db: Database):
29+
item = db[REC_ID]
30+
logger.debug(item)
31+
assert type(item) is dict
32+
assert item['randomInteger'] == 123
33+
assert type(db[REC_ID_NOT_EXIST]) is type(None)
34+
35+
36+
def test_getter_by_list(db: Database):
37+
items = db[[REC_ID, REC_ID_2]]
38+
logger.debug(items)
39+
assert type(items) is list
40+
assert len(items) == 2
41+
items = db[[REC_ID_NOT_EXIST]]
42+
assert items == [None]
43+
44+
45+
def test_setter(db: Database):
46+
assert db[REC_ID]['randomInteger'] == 123
47+
try:
48+
db[REC_ID] = {'test': True}
49+
except NotImplementedError:
50+
pass
51+
except:
52+
fail()
53+
54+
55+
def test_del(db: Database):
56+
try:
57+
del db[REC_ID]
58+
except:
59+
fail()
60+
assert db[REC_ID] is None
61+
62+
63+
def test_len(db: Database):
64+
assert type(len(db)) is int
65+
assert len(db) == 2
66+
67+
68+
def test_items(db: Database):
69+
assert type(db.items()) is type(dict().items())
70+
71+
72+
def test_keys(db: Database):
73+
assert type(db.keys()) is type(dict().keys())
74+
75+
76+
def test_values(db: Database):
77+
assert type(db.values()) is type(dict().values())
78+
79+
80+
def test_repr(db: Database):
81+
assert repr(db) == DB_STR_OUTPUT
82+
83+
84+
def test_str(db: Database):
85+
assert str(db) == DB_STR_OUTPUT
86+
87+
88+
def test_contains(db: Database):
89+
"""The `in` keyword is used to check if a value is present in a sequence (list, range, string etc.).
90+
"""
91+
assert True == (REC_ID in db)
92+
assert True == (REC_ID_2 in db)
93+
assert False == (REC_ID_NOT_EXIST in db)
94+
95+
96+
def test_constructor():
97+
extenral_dict = dict(age=25, name='Tom')
98+
try:
99+
db = Database(extenral_dict)
100+
except:
101+
fail()
102+
103+
104+
def test_deconstructor():
105+
db = Database(dict())
106+
try:
107+
del db
108+
except:
109+
fail()

0 commit comments

Comments
 (0)