|
1 | 1 | from __future__ import annotations |
2 | 2 | from abc import ABC |
3 | | -from typing import get_type_hints, get_origin, get_args, TypeVar |
4 | | -from types import UnionType, NoneType, |
| 3 | +from typing import get_type_hints, get_origin, get_args, TypeVar, Tuple, Type |
| 4 | +from types import UnionType, NoneType |
5 | 5 | from . import io_safe |
6 | 6 |
|
7 | | -T = TypeVar('T') |
8 | 7 |
|
9 | 8 |
|
| 9 | +T = TypeVar("T") |
| 10 | +T2 = TypeVar("T2") |
10 | 11 |
|
11 | | -class FolderBase(ABC): |
12 | | - __folder__ = None |
13 | | - __file__base__ = None |
14 | 12 |
|
| 13 | +def get_type_hints_excluding_internals(cls): |
| 14 | + type_hints = {} |
| 15 | + for var_name, var_type in get_type_hints(cls).items(): |
| 16 | + if var_name.startswith("__") and var_name.endswith("__"): |
| 17 | + continue |
| 18 | + type_hints[var_name] = var_type |
| 19 | + return type_hints |
15 | 20 |
|
16 | | -class FileDictBase(ABC): |
| 21 | + |
| 22 | + |
| 23 | + |
| 24 | + |
| 25 | + |
| 26 | +def fill_object_from_dict_using_type_hints(obj, cls, data: dict): |
| 27 | + """ |
| 28 | + Attributes of obj are set using the data dict. |
| 29 | + The type hints of the class cls are used to determine which attributes to set. |
| 30 | + """ |
| 31 | + for var_name, var_type in get_type_hints_excluding_internals(cls).items(): |
| 32 | + # Check if variable is nullable (e.g. email: str | None) |
| 33 | + nullable = get_origin(var_type) is UnionType and NoneType in get_args(var_type) |
| 34 | + # When it is not nullable but not in the data, raise an error |
| 35 | + if var_name not in data: |
| 36 | + print(var_name, get_origin(var_type), get_args(var_type)) |
| 37 | + if not nullable: |
| 38 | + raise RuntimeError(f"Missing variable '{var_name}' in {cls.__name__}.") |
| 39 | + else: |
| 40 | + continue |
| 41 | + # When it is a list, fill the list with the items |
| 42 | + if get_origin(var_type) is list and len(arg := get_args(var_type)) == 1: |
| 43 | + item_type = arg[0] |
| 44 | + setattr(obj, var_name, [item_type.from_dict(x) for x in data[var_name]]) |
| 45 | + else: |
| 46 | + setattr(obj, var_name, data.get(var_name, None)) |
| 47 | + return obj |
| 48 | + |
| 49 | + |
| 50 | + |
| 51 | + |
| 52 | + |
| 53 | +######################################################################################## |
| 54 | +# Scenario 1: |
| 55 | +# Model a single file with FileDictModel, which is a dict at the top level. |
| 56 | +# Each key-value item is modeled by a DictItemModel. |
| 57 | + |
| 58 | + |
| 59 | + |
| 60 | +class FileDictModel(ABC): |
17 | 61 | """ |
18 | 62 | A file base refers to a file that is stored in the database. |
| 63 | + At the top level the file must contain a dictionary with strings as keys. |
19 | 64 | """ |
20 | 65 |
|
21 | 66 | __file__ = None |
22 | | - __item_base__: T = None |
23 | | - __create_file_on_write__ = False |
24 | | - |
| 67 | + __item_model__: T = None |
25 | 68 |
|
26 | 69 | @classmethod |
27 | 70 | def get_by_key(cls, key) -> T: |
28 | 71 | """ |
29 | 72 | Gets an item by key. |
| 73 | + The data is partially read from the __file__. |
30 | 74 | """ |
31 | | - return cls.__item_base__.get_by_key(key) |
32 | | - |
| 75 | + return cls.__item_model__.partial_read_by_key(key) |
33 | 76 |
|
| 77 | + @classmethod |
| 78 | + def items(cls) -> list[Tuple[str, T]]: |
| 79 | + """ |
| 80 | + Gets all items. |
| 81 | + """ |
| 82 | + data = io_safe.read(cls.__file__) |
| 83 | + return [cls.__item_model__(key, value) for key, value in data.items()] |
34 | 84 |
|
| 85 | + @classmethod |
| 86 | + def session(cls): |
| 87 | + ... |
35 | 88 |
|
36 | 89 |
|
37 | | -class FileDictItemBase(ABC): |
38 | 90 | @classmethod |
39 | | - def get_by_key(cls, key): |
40 | | - data = io_safe.partial_read(cls.__file__, key) |
41 | | - return cls.from_data(data) |
| 91 | + def session_at_key(cls, key): |
| 92 | + ... |
| 93 | + |
42 | 94 |
|
43 | 95 |
|
44 | 96 |
|
45 | 97 | @classmethod |
46 | | - def from_data(cls, data): |
| 98 | + def get_where(cls, where: callable) -> list[T]: |
| 99 | + """ |
| 100 | + Gets all items where the where function returns True. |
| 101 | + The where function takes an object of type __item_model__. |
| 102 | + """ |
| 103 | + return [item for item in cls.get_all() if where(item)] |
47 | 104 |
|
48 | | - instance = cls() |
| 105 | + # Not Implemented: |
| 106 | + # - select by filter callback (file_where): Not implemented because no performance advantage. |
49 | 107 |
|
50 | | - for var_name, var_type in get_type_hints(cls).items(): |
51 | | - # Check if variable is nullable (e.g. email: str | None) |
52 | | - nullable = get_origin(var_type) is UnionType and NoneType in get_args(var_name) |
53 | | - # When it is not nullable but not in the data, raise an error |
54 | | - if var_name not in data and not nullable: |
55 | | - raise RuntimeError(f"Missing variable '{var_name}' in file '{cls.__file__}'.") |
56 | 108 |
|
57 | | - setattr(instance, var_name, data.get(var_name, None)) |
58 | 109 |
|
59 | | - return instance |
| 110 | +class FileDictItemModel(ABC): |
| 111 | + __key__: str |
60 | 112 |
|
| 113 | + @classmethod |
| 114 | + def from_key_value(cls: Type[T2], key, value) -> T2: |
| 115 | + obj = fill_object_from_dict_using_type_hints(cls(), cls, value) |
| 116 | + obj.__key__ = key |
| 117 | + return obj |
61 | 118 |
|
| 119 | + @classmethod |
| 120 | + def read_by_key(cls, key) -> T: |
| 121 | + data = io_safe.partial_read(cls.__file__, key) |
| 122 | + return cls.from_key_value(key, data) |
62 | 123 |
|
63 | 124 |
|
64 | 125 |
|
65 | 126 |
|
66 | | -class User(FileDictItemBase): |
67 | | - first_name: str | None |
68 | | - last_name: str |
69 | | - email: str |
| 127 | +class DictModel(ABC): |
| 128 | + |
| 129 | + @classmethod |
| 130 | + def from_dict(cls, data) -> DictModel: |
| 131 | + obj = cls() |
| 132 | + return fill_object_from_dict_using_type_hints(obj, cls, data) |
| 133 | + |
70 | 134 |
|
71 | | - def full_name(self): |
72 | | - return f"{self.first_name} {self.last_name}" |
| 135 | + def to_dict(self) -> dict: |
| 136 | + res = {} |
| 137 | + for var_name in get_type_hints(self).keys(): |
| 138 | + if (value := getattr(self, var_name)) is not None: |
| 139 | + res[var_name] = value |
| 140 | + return res |
73 | 141 |
|
74 | 142 |
|
75 | | -class Users(FileDictBase): |
76 | | - __file__ = "users" |
77 | | - __item_base__ = User |
78 | 143 |
|
79 | 144 |
|
| 145 | +######################################################################################## |
| 146 | +# Scenario 2: |
| 147 | +# Add in a later version of DDB |
| 148 | +# A folder containing multiple files, each containing json. |
80 | 149 |
|
81 | | -# Iterate FileDictBase |
82 | | -# for user_id, user: User in Users.items(): |
83 | | -# print(user_id, user.first_name, user.last_name, user.email) |
| 150 | +# class FolderBase(ABC): |
| 151 | +# __folder__ = None |
| 152 | +# __file_model__: FileInFolderModel = None |
84 | 153 |
|
85 | | -# # Get one item |
86 | | -# user: User = Users.get("user_id") |
87 | 154 |
|
| 155 | +# class FileInFolderModel(ABC): |
88 | 156 |
|
89 | | -# Get by lambda |
90 | | -# users: Users = Users.where(lambda user: user.first_name != "John") |
| 157 | +# @classmethod |
| 158 | +# def get_by_name(cls, file_name: str) -> FileInFolderModel: |
| 159 | +# data = io_safe.read(f"{cls.__folder__}/{file_name}") |
| 160 | +# return cls(**data) |
0 commit comments