Skip to content

Commit

Permalink
version: 0.2.4 (#11)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
joonas-yoon authored Jan 25, 2023
2 parents 0c13c7c + 9da148d commit 4853c99
Show file tree
Hide file tree
Showing 19 changed files with 392 additions and 68 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/pytest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@ on:
push:
branches:
- 'main'
- 'dev'
pull_request:
types:
- opened
- edited
- reopened
- review_requested
- ready_for_review
branches:
- 'main'
Expand Down
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Supports to find by key and value with `Condition` that is generated from `Key`.
- 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)
- Implements useful representation for quickly displaying as a table format. [(#4)](https://github.com/joonas-yoon/json-as-db/issues/4)
- Add static method to load - `json_as_db.load(path)` [(#7)](https://github.com/joonas-yoon/json-as-db/issues/7)
- Add a variable `__version__` in global.

### Fixed

- Implements `items()` methods to override dictionary on `Database` class. [(#3)](https://github.com/joonas-yoon/json-as-db/issues/3)
- Getter supports list-like parameter such as `db[['id1', 'id2']]` [(#5)](https://github.com/joonas-yoon/json-as-db/issues/5)

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

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "json_as_db"
version = "0.2.3"
version = "0.2.4"
description = "Using JSON as very lightweight database"
readme = "README.md"
license = "MIT"
Expand Down
6 changes: 4 additions & 2 deletions src/json_as_db/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from .core.database import Database
from .core.matcher import Condition, Conditions, Key

__version__ = '0.2.4'
from .statics import load
from .constants import __version__

__all__ = [
"__version__",
"Condition",
"Conditions",
"Database",
"Key",
"load",
]
File renamed without changes.
8 changes: 8 additions & 0 deletions src/json_as_db/_utils/decorater.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from typing import Callable


def copy_doc(original: Callable) -> Callable:
def wrapper(target: Callable) -> Callable:
target.__doc__ = original.__doc__
return target
return wrapper
2 changes: 2 additions & 0 deletions src/json_as_db/constants.py
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
package_name = "json_as_db"

__version__ = '0.2.4'
106 changes: 106 additions & 0 deletions src/json_as_db/core/_formatting.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
from typing import List, Tuple


def split_first_last(l: list, k: int) -> Tuple[list, list]:
_len = len(l)
if _len <= 1:
return l, []
is_odd = k % 2
k = min(k // 2, _len // 2)
return l[:(k+int(is_odd))], l[-k:]


def to_plural(unit_str: str, k: int) -> str:
return unit_str + ('' if k < 2 else 's')


def collapsed_row(l: list, is_skip: bool) -> list:
_first, _last = split_first_last(l, len(l))
return _first + (['...'] if is_skip else []) + _last


def collapse_str(s: str, width: int) -> str:
if len(s) <= width:
return s
return s[:min(len(s), width - 3)] + '...'


def row_str(l: list, delimiter: str = '') -> str:
return f" {delimiter} ".join(l) + "\n"


def row_padded(row: list, widths: list) -> List[str]:
return [row[i].ljust(widths[i]) for i in range(len(row))]


def stringify(all_items: List[dict]) -> str:
"""Return 3 each rows from the top and the bottom.
Returns:
str: The first and last 3 rows of the caller object.
Example:
>>> db
age grouped ... job name
32 True ... Camera operator Layne
17 False ... Flying instructor Somerled
9 True ... Inventor Joon-Ho
... ... ... ... ...
23 None ... Publican Melanie
54 True ... Racing driver Eike
41 None ... Barrister Tanja
[100 items, 9 keys]
"""
# Collect key names to be column
keys = set()
for item in all_items:
keys |= set(item.keys())
total_rows = len(all_items)
total_cols = len(keys)

# Display options
keys = sorted(list(keys))
rows_display = 6
cols_display = 4
text_max_width = 12

# Collect rows by columns and collapse them
first_cols, last_cols = split_first_last(keys, cols_display)
first_rows, last_rows = split_first_last(all_items, rows_display)
rows = first_rows + last_rows
cols = first_cols + last_cols
col_widths = [max(3, len(str(col))) for col in cols]
table = []
for irow, row in enumerate(rows):
t_row = []
for icol, col in enumerate(cols):
try:
stringified = str(row[col])
except KeyError:
stringified = str(None)
text = collapse_str(stringified, width=text_max_width)
col_widths[icol] = max(col_widths[icol], len(text))
t_row.append(text)
table.append(t_row)

# Shortcut functions
def _clp_row(l): return collapsed_row(l, is_skip=total_cols > cols_display)
def _make_row_str(l): return row_str(_clp_row(l), delimiter='')
def _padded(l): return row_padded(l, widths=col_widths)

# Create result strings
result = _make_row_str(_padded(first_cols + last_cols))
for irow, row in enumerate(table):
s = _padded(row)
result += _make_row_str(s)
if total_rows > rows_display and irow + 1 == len(first_rows):
result += _make_row_str(_padded(['...'] * (len(cols))))

result += "\n\n" + ", ".join([
f"[{total_rows} {to_plural('item', total_rows)}",
f"{total_cols} {to_plural('key', total_cols)}]",
])

return result
50 changes: 37 additions & 13 deletions src/json_as_db/core/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
from datetime import datetime
from typing import Any, Union, List, Callable

from ..constants import package_name
from .._utils import override_dict, from_maybe_list, return_maybe
from ..constants import package_name, __version__
from .._utils import override_dict, from_maybe_list, return_maybe, decorater
from ._formatting import stringify

__all__ = [
'Database'
Expand All @@ -16,7 +17,6 @@

class Database(dict):
__data__ = 'data'
__version__ = '1.0.0'
__metadata__ = [
'version',
'creator',
Expand All @@ -34,18 +34,15 @@ def __init__(self, *arg, **kwargs) -> None:
def _create_empty(self) -> dict:
now = datetime.now().isoformat()
return {
'version': self.__version__,
'version': __version__,
'creator': package_name,
'created_at': now,
'updated_at': now,
self.__data__: dict(),
}

def __getitem__(self, key: str) -> Any:
try:
return self.data.__getitem__(key)
except KeyError:
return None
def __getitem__(self, key: Union[str, List[str]]) -> Union[Any, List[Any]]:
return self.get(key)

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

def __repr__(self):
return self.__exports_only_publics().__repr__()
@decorater.copy_doc(stringify)
def __repr__(self) -> str:
return stringify(list(self.data.values()))

def __str__(self):
@decorater.copy_doc(stringify)
def __str__(self) -> str:
return str(self.__repr__())

def __len__(self):
def __len__(self) -> int:
return len(self.data)

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

def items(self) -> list:
return self.data.items()

def values(self) -> list:
return self.data.values()

@property
def version(self) -> str:
return self.__dict__.get('version')

@property
def data(self) -> dict:
return self.__dict__.get(self.__data__)
Expand All @@ -100,6 +106,24 @@ def _update_timestamp(self) -> None:
})

def get(self, key: Union[str, List[str]], default=None) -> Union[Any, List[Any]]:
"""Get objects by given IDs when list is given.
When single string is given, returns single object by given key
Args:
key (str | List[str]): single key or list-like
default (Any, optional): default value if not exists. Defaults to None.
Returns:
Any | List[Any]: single object or list-like
Examples:
>>> db.get('kcbPuqpfV3YSHT8YbECjvh')
{...}
>>> db.get(['kcbPuqpfV3YSHT8YbECjvh'])
[{...}]
>>> db.get(['kcbPuqpfV3YSHT8YbECjvh', 'jmJKBJBAmGESC3rGbSb62T'])
[{...}, {...}]
"""
_type, _keys = from_maybe_list(key)
values = [self.data.get(k, default) for k in _keys]
return return_maybe(_type, values)
Expand Down
6 changes: 6 additions & 0 deletions src/json_as_db/statics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from .core.database import Database


def load(path: str, *args, **kwargs) -> Database:
return Database().load(path, *args, **kwargs)

109 changes: 109 additions & 0 deletions tests/database/test_dict.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import os
import pytest

from utils import file, logger, fail

from json_as_db import Database


CUR_DIR = os.path.dirname(os.path.realpath(__file__))
DB_FILENAME = 'db.json'
DB_FILEPATH = os.path.join(CUR_DIR, '..', 'samples', DB_FILENAME)
REC_ID = 'kcbPuqpfV3YSHT8YbECjvh'
REC_ID_2 = 'jmJKBJBAmGESC3rGbSb62T'
REC_ID_NOT_EXIST = 'N0t3xIstKeyV41ueString'
DB_STR_OUTPUT = """booleanFalse booleanTrue ... randomInteger randomString
False True ... 123 keyboard-cat
None None ... 321 cheshire-cat
[2 items, 6 keys]"""


@pytest.fixture()
def db() -> Database:
return Database().load(DB_FILEPATH)


def test_getter(db: Database):
item = db[REC_ID]
logger.debug(item)
assert type(item) is dict
assert item['randomInteger'] == 123
assert type(db[REC_ID_NOT_EXIST]) is type(None)


def test_getter_by_list(db: Database):
items = db[[REC_ID, REC_ID_2]]
logger.debug(items)
assert type(items) is list
assert len(items) == 2
items = db[[REC_ID_NOT_EXIST]]
assert items == [None]


def test_setter(db: Database):
assert db[REC_ID]['randomInteger'] == 123
try:
db[REC_ID] = {'test': True}
except NotImplementedError:
pass
except:
fail()


def test_del(db: Database):
try:
del db[REC_ID]
except:
fail()
assert db[REC_ID] is None


def test_len(db: Database):
assert type(len(db)) is int
assert len(db) == 2


def test_items(db: Database):
assert type(db.items()) is type(dict().items())


def test_keys(db: Database):
assert type(db.keys()) is type(dict().keys())


def test_values(db: Database):
assert type(db.values()) is type(dict().values())


def test_repr(db: Database):
assert repr(db) == DB_STR_OUTPUT


def test_str(db: Database):
assert str(db) == DB_STR_OUTPUT


def test_contains(db: Database):
"""The `in` keyword is used to check if a value is present in a sequence (list, range, string etc.).
"""
assert True == (REC_ID in db)
assert True == (REC_ID_2 in db)
assert False == (REC_ID_NOT_EXIST in db)


def test_constructor():
extenral_dict = dict(age=25, name='Tom')
try:
db = Database(extenral_dict)
except:
fail()


def test_deconstructor():
db = Database(dict())
try:
del db
except:
fail()
Loading

0 comments on commit 4853c99

Please sign in to comment.