syncwave 0.1.3__tar.gz → 0.1.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: syncwave
3
- Version: 0.1.3
3
+ Version: 0.1.4
4
4
  Summary: Make your code reactive; Turn plain JSONs into a live data store, two-way synced with Python objects.
5
5
  Keywords:
6
6
  Author: Lucas Iannitello
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "syncwave"
3
- version = "0.1.3"
3
+ version = "0.1.4"
4
4
  description = "Make your code reactive; Turn plain JSONs into a live data store, two-way synced with Python objects."
5
5
  readme = "README.md"
6
6
  authors = [{ name = "Lucas Iannitello", email = "lucas.iannitello@protonmail.com" }]
@@ -28,8 +28,24 @@ dev = [
28
28
  ]
29
29
 
30
30
  [tool.ruff.lint]
31
- select = ["E", "F"]
31
+ select = [
32
+ "E", # pycodestyle errors
33
+ "F", # pyflakes
34
+ "I", # isort
35
+ "B", # flake8-bugbear
36
+ "C4", # flake8-comprehensions
37
+ "SIM", # flake8-simplify
38
+ "ANN", # flake8-annotations
39
+ "RUF", # ruff
40
+ "UP", # pyupgrade
41
+ ]
32
42
 
33
43
  unfixable = [
34
44
  "F401" # disable fix for unused imports
35
45
  ]
46
+
47
+ [tool.ruff.lint.per-file-ignores]
48
+ "__init__.py" = ["I002"]
49
+
50
+ [tool.ruff.lint.isort]
51
+ required-imports = ["from __future__ import annotations"]
@@ -1,4 +1,4 @@
1
- __version__ = "0.1.3"
1
+ __version__ = "0.1.4"
2
2
 
3
3
 
4
4
  from .syncwave import SyncStore, Syncwave
@@ -1,9 +1,12 @@
1
+ from __future__ import annotations
2
+
3
+ import contextlib
1
4
  import json
2
5
  import os
3
6
  from pathlib import Path
4
7
  from tempfile import mkstemp
5
8
  from threading import Lock, Timer
6
- from typing import Any, Callable
9
+ from typing import Any, Callable, Final
7
10
 
8
11
  from .watcher import watcher
9
12
 
@@ -12,10 +15,10 @@ DataProvider = Callable[[], JSONData]
12
15
 
13
16
 
14
17
  class _IO:
15
- ENCODING = "utf-8"
16
- DEFAULT_JSON_CONTENT = {}
17
- DUMPS_CONFIG = {"indent": 2, "ensure_ascii": False}
18
- DEBOUNCE_WINDOW = 0.05
18
+ ENCODING: Final[str] = "utf-8"
19
+ DEFAULT_JSON_CONTENT: Final[JSONData] = {}
20
+ DUMPS_CONFIG: Final[dict[str, Any]] = {"indent": 2, "ensure_ascii": False}
21
+ DEBOUNCE_WINDOW: Final[float] = 0.05
19
22
 
20
23
  def __init__(self) -> None:
21
24
  self._lock = Lock()
@@ -37,7 +40,7 @@ class _IO:
37
40
  with path.open(encoding=self.ENCODING) as f:
38
41
  json.load(f)
39
42
  except json.JSONDecodeError as e:
40
- raise IOError(f"File '{path}' exists but is not a valid JSON file.") from e
43
+ raise OSError(f"File '{path}' exists but is not a valid JSON file.") from e
41
44
 
42
45
  def read_json(self, path: Path) -> JSONData:
43
46
  with self._lock:
@@ -83,12 +86,10 @@ class _IO:
83
86
 
84
87
  except Exception as e:
85
88
  # Attempt to remove temporary file on error
86
- try:
89
+ with contextlib.suppress(OSError):
87
90
  os.remove(tmp_path)
88
- except OSError:
89
- pass
90
91
  # Raise a clear error message including context
91
- raise IOError(
92
+ raise OSError(
92
93
  f"Failed to write JSON to '{path}'. "
93
94
  f"Temporary file '{tmp_path}' removed. Original error: {e}"
94
95
  ) from e
@@ -1,9 +1,11 @@
1
- from collections.abc import Mapping
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterator, Mapping
2
4
  from inspect import isclass
3
5
  from json import JSONDecodeError
4
6
  from pathlib import Path
5
7
  from threading import RLock
6
- from typing import Any, Callable, Iterator, Optional, Type, Union, get_args, overload
8
+ from typing import Any, Callable, Union, get_args, overload
7
9
 
8
10
  from pydantic import BaseModel, ValidationError, create_model, model_validator
9
11
 
@@ -14,7 +16,7 @@ from .watcher import watcher
14
16
  JSONKey = Union[str, int, float, bool, None]
15
17
 
16
18
 
17
- def _validate_key_type(cls: Type[BaseModel], key: str) -> None:
19
+ def _validate_key_type(cls: type[BaseModel], key: str) -> None:
18
20
  # This will work if the key field is a basic type (str, int, float, bool, or None),
19
21
  # but will fail for more complex types (Union[str, int], Optional[str], etc.).
20
22
  # In that case skip the validation.
@@ -31,17 +33,17 @@ def _validate_key_type(cls: Type[BaseModel], key: str) -> None:
31
33
  class SyncStore(Mapping[JSONKey, BaseModel]):
32
34
  def __init__(
33
35
  self,
34
- syncwave: "Syncwave",
35
- cls: Type[BaseModel],
36
+ syncwave: Syncwave,
37
+ cls: type[BaseModel],
36
38
  /,
37
- name: Optional[str] = None,
38
- key: Optional[str] = None,
39
+ name: str | None = None,
40
+ key: str | None = None,
39
41
  *,
40
42
  skip_key_validation: bool = False,
41
- file_name: Optional[str] = None,
42
- sub_dir: Optional[Union[Path, str]] = None,
43
- file_path: Optional[Union[Path, str]] = None,
44
- new_cls_name: Optional[str] = None,
43
+ file_name: str | None = None,
44
+ sub_dir: Path | str | None = None,
45
+ file_path: Path | str | None = None,
46
+ new_cls_name: str | None = None,
45
47
  # in_memory: bool = True, # TODO: implement in_memory
46
48
  ) -> None:
47
49
  if not isinstance(syncwave, Syncwave):
@@ -123,8 +125,8 @@ class SyncStore(Mapping[JSONKey, BaseModel]):
123
125
  def _get_path(
124
126
  self,
125
127
  file_name: str,
126
- sub_dir: Union[Path, str, None],
127
- file_path: Union[Path, str, None],
128
+ sub_dir: Path | str | None,
129
+ file_path: Path | str | None,
128
130
  ) -> Path:
129
131
  if file_path:
130
132
  return Path(file_path).resolve()
@@ -133,7 +135,7 @@ class SyncStore(Mapping[JSONKey, BaseModel]):
133
135
  return (self.syncwave.data_dir / sub_dir / file_name).resolve()
134
136
  return (self.syncwave.data_dir / file_name).resolve()
135
137
 
136
- def _patch_cls(self, cls: Type[BaseModel], new_cls_name: str) -> Type[BaseModel]:
138
+ def _patch_cls(self, cls: type[BaseModel], new_cls_name: str) -> type[BaseModel]:
137
139
  # This is a validator that acts as a post-initialization hook.
138
140
  # Using a validator instead of `__init__` ensures every instance is tracked.
139
141
  # Libraries such as FastAPI may create instances indirectly with methods like
@@ -157,7 +159,7 @@ class SyncStore(Mapping[JSONKey, BaseModel]):
157
159
  original_setattr = cls.__setattr__
158
160
  original_delattr = cls.__delattr__
159
161
 
160
- def __setattr__(self_model: BaseModel, attr: str, value: Any) -> None:
162
+ def __setattr__(self_model: BaseModel, attr: str, value: Any) -> None: # noqa: ANN401
161
163
  # TODO check for edge cases
162
164
  old_value = getattr(self_model, attr)
163
165
  original_setattr(self_model, attr, value)
@@ -183,7 +185,7 @@ class SyncStore(Mapping[JSONKey, BaseModel]):
183
185
 
184
186
 
185
187
  class Syncwave(Mapping[str, SyncStore]):
186
- def __init__(self, data_dir: Union[str, Path]) -> None:
188
+ def __init__(self, data_dir: str | Path) -> None:
187
189
  self.data_dir = Path(data_dir).resolve()
188
190
  self._swd: dict[str, SyncStore] = {} # actual mapping, swd = SyncWaveDict
189
191
 
@@ -216,28 +218,28 @@ class Syncwave(Mapping[str, SyncStore]):
216
218
  def register(
217
219
  self,
218
220
  *,
219
- name: Optional[str] = None,
220
- key: Optional[str] = None,
221
+ name: str | None = None,
222
+ key: str | None = None,
221
223
  skip_key_validation: bool = False,
222
- file_name: Optional[str] = None,
223
- sub_dir: Optional[Union[Path, str]] = None,
224
- file_path: Optional[Union[Path, str]] = None,
225
- ) -> Callable[[Type[BaseModel]], Type[BaseModel]]: ...
224
+ file_name: str | None = None,
225
+ sub_dir: Path | str | None = None,
226
+ file_path: Path | str | None = None,
227
+ ) -> Callable[[type[BaseModel]], type[BaseModel]]: ...
226
228
 
227
229
  def register(
228
230
  self,
229
- _cls: Optional[Type[BaseModel]] = None,
231
+ _cls: type[BaseModel] | None = None,
230
232
  /,
231
233
  *,
232
- name: Optional[str] = None,
233
- key: Optional[str] = None,
234
+ name: str | None = None,
235
+ key: str | None = None,
234
236
  skip_key_validation: bool = False,
235
- file_name: Optional[str] = None,
236
- sub_dir: Optional[Union[Path, str]] = None,
237
- file_path: Optional[Union[Path, str]] = None,
237
+ file_name: str | None = None,
238
+ sub_dir: Path | str | None = None,
239
+ file_path: Path | str | None = None,
238
240
  # in_memory: bool = True, # TODO: implement in_memory
239
- ) -> Union[Type[BaseModel], Callable[[Type[BaseModel]], Type[BaseModel]]]:
240
- def decorator(cls: Type[BaseModel]) -> Type[BaseModel]:
241
+ ) -> type[BaseModel] | Callable[[type[BaseModel]], type[BaseModel]]:
242
+ def decorator(cls: type[BaseModel]) -> type[BaseModel]:
241
243
  store = SyncStore(
242
244
  self,
243
245
  cls,
@@ -1,7 +1,9 @@
1
+ from __future__ import annotations
2
+
1
3
  from pathlib import Path
2
4
  from threading import Lock, Timer
3
5
  from time import monotonic, sleep
4
- from typing import Callable
6
+ from typing import Callable, Final
5
7
 
6
8
  from watchdog.events import (
7
9
  DirDeletedEvent,
@@ -18,7 +20,7 @@ Callback = Callable[[], None]
18
20
 
19
21
 
20
22
  class _Watcher:
21
- TMP_FILE_PREFIX = ".tmp_"
23
+ TMP_FILE_PREFIX: Final[str] = ".tmp_"
22
24
 
23
25
  def __init__(self) -> None:
24
26
  self._lock = Lock()
@@ -96,8 +98,8 @@ class _Watcher:
96
98
 
97
99
 
98
100
  class _EventHandler(FileSystemEventHandler):
99
- DEBOUNCE_WINDOW = 0.05
100
- SELF_WRITE_WINDOW = 0.5
101
+ DEBOUNCE_WINDOW: Final[float] = 0.05
102
+ SELF_WRITE_WINDOW: Final[float] = 0.5
101
103
 
102
104
  def __init__(self, watcher: _Watcher) -> None:
103
105
  self._watcher = watcher
@@ -121,9 +123,7 @@ class _EventHandler(FileSystemEventHandler):
121
123
  def _is_self_write(self, file_path: FilePath) -> bool:
122
124
  # not protected by the lock because it's only called internally
123
125
  ts = self._self_writes_ts.get(file_path)
124
- if ts is not None and monotonic() - ts < self.SELF_WRITE_WINDOW:
125
- return True
126
- return False
126
+ return ts is not None and monotonic() - ts < self.SELF_WRITE_WINDOW
127
127
 
128
128
  def dispatch(self, event: FileSystemEvent) -> None:
129
129
  if event.is_directory:
File without changes
File without changes
File without changes