sqlite-database 0.7.7__tar.gz → 0.7.9__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.
Files changed (112) hide show
  1. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/.gitignore +1 -0
  2. {sqlite_database-0.7.7/sqlite_database.egg-info → sqlite_database-0.7.9}/PKG-INFO +24 -1
  3. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/README.md +23 -0
  4. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/TODO.md +5 -5
  5. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/sqlite_database/__init__.py +2 -1
  6. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/sqlite_database/_utils.py +8 -0
  7. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/sqlite_database/csv.py +13 -1
  8. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/sqlite_database/database.py +3 -30
  9. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/sqlite_database/errors.py +11 -0
  10. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/sqlite_database/models/__init__.py +3 -2
  11. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/sqlite_database/table.py +74 -17
  12. sqlite_database-0.7.9/sqlite_database/workers/__init__.py +5 -0
  13. sqlite_database-0.7.9/sqlite_database/workers/connection.py +214 -0
  14. sqlite_database-0.7.9/sqlite_database/workers/database.py +62 -0
  15. {sqlite_database-0.7.7 → sqlite_database-0.7.9/sqlite_database.egg-info}/PKG-INFO +24 -1
  16. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/sqlite_database.egg-info/SOURCES.txt +3 -0
  17. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/tests/database/table_api/test_others.py +14 -1
  18. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/.editorconfig +0 -0
  19. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  20. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  21. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/.github/ISSUE_TEMPLATE/question.md +0 -0
  22. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/.github/dependabot.yml +0 -0
  23. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/.github/workflows/pylint.yml +0 -0
  24. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/.github/workflows/pytest.yml +0 -0
  25. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/.github/workflows/python-publish.yml +0 -0
  26. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/.readthedocs.yaml +0 -0
  27. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/.vscode/settings.json +0 -0
  28. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/Features.md +0 -0
  29. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/History.md +0 -0
  30. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/LICENSE +0 -0
  31. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/SimpleGuide.md +0 -0
  32. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/bin/activate +0 -0
  33. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/bin/check.bat +0 -0
  34. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/bin/check.sh +0 -0
  35. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/bin/include/utility.bash +0 -0
  36. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/bin/install.bash +0 -0
  37. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/bin/need-installed/activate +0 -0
  38. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/bin/need-installed/pre-commit +0 -0
  39. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/bin/summarize-pylint.py +0 -0
  40. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/dev-config/black.toml +0 -0
  41. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/dev-config/pylint.toml +0 -0
  42. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/dev-config/pytest.ini +0 -0
  43. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/dev-requirements.txt +0 -0
  44. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/docs/Makefile +0 -0
  45. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/docs/ModelAPI.md +0 -0
  46. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/docs/SimpleGuide.md +0 -0
  47. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/docs/_.md +0 -0
  48. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/docs/api_reference.rst +0 -0
  49. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/docs/conf.py +0 -0
  50. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/docs/index.rst +0 -0
  51. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/docs/make.bat +0 -0
  52. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/docs/modules.rst +0 -0
  53. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/docs/sqlite_database.column.rst +0 -0
  54. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/docs/sqlite_database.config.rst +0 -0
  55. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/docs/sqlite_database.csv.rst +0 -0
  56. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/docs/sqlite_database.database.rst +0 -0
  57. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/docs/sqlite_database.errors.rst +0 -0
  58. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/docs/sqlite_database.functions.rst +0 -0
  59. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/docs/sqlite_database.locals.rst +0 -0
  60. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/docs/sqlite_database.model.errors.rst +0 -0
  61. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/docs/sqlite_database.model.helpers.rst +0 -0
  62. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/docs/sqlite_database.model.query_builder.rst +0 -0
  63. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/docs/sqlite_database.model.rst +0 -0
  64. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/docs/sqlite_database.operators.rst +0 -0
  65. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/docs/sqlite_database.query_builder.rst +0 -0
  66. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/docs/sqlite_database.rst +0 -0
  67. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/docs/sqlite_database.signature.rst +0 -0
  68. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/docs/sqlite_database.subexp.rst +0 -0
  69. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/docs/sqlite_database.table.rst +0 -0
  70. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/docs/sqlite_database.typings.rst +0 -0
  71. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/docs/sqlite_database.utils.rst +0 -0
  72. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/docs-requirements.txt +0 -0
  73. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/project-init.bash +0 -0
  74. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/pyproject.toml +0 -0
  75. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/setup.cfg +0 -0
  76. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/setup.py +0 -0
  77. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/sqlite_database/_debug.py +0 -0
  78. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/sqlite_database/column.py +0 -0
  79. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/sqlite_database/functions.py +0 -0
  80. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/sqlite_database/locals.py +0 -0
  81. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/sqlite_database/models/errors.py +0 -0
  82. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/sqlite_database/models/helpers.py +0 -0
  83. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/sqlite_database/models/mixin.py +0 -0
  84. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/sqlite_database/models/query_builder.py +0 -0
  85. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/sqlite_database/models/type_checkers.py +0 -0
  86. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/sqlite_database/operators.py +0 -0
  87. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/sqlite_database/query_builder.py +0 -0
  88. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/sqlite_database/signature.py +0 -0
  89. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/sqlite_database/subquery.py +0 -0
  90. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/sqlite_database/typings.py +0 -0
  91. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/sqlite_database/utils.py +0 -0
  92. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/sqlite_database.egg-info/dependency_links.txt +0 -0
  93. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/sqlite_database.egg-info/requires.txt +0 -0
  94. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/sqlite_database.egg-info/top_level.txt +0 -0
  95. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/sqlite_database.egg-info/zip-safe +0 -0
  96. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/tests/__init__.py +0 -0
  97. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/tests/database/__init__.py +0 -0
  98. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/tests/database/model_api/__init__.py +0 -0
  99. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/tests/database/model_api/test_model_api.py +0 -0
  100. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/tests/database/setup.py +0 -0
  101. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/tests/database/table_api/__init__.py +0 -0
  102. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/tests/database/table_api/test_csv.py +0 -0
  103. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/tests/database/table_api/test_delete.py +0 -0
  104. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/tests/database/table_api/test_insert.py +0 -0
  105. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/tests/database/table_api/test_select.py +0 -0
  106. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/tests/database/table_api/test_update.py +0 -0
  107. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/tests/database/test_custom.py +0 -0
  108. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/tests/manual_test_performances.py +0 -0
  109. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/tests/test_internals.py +0 -0
  110. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/tests/user_benchmark.py +0 -0
  111. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/tests/user_helpers.py +0 -0
  112. {sqlite_database-0.7.7 → sqlite_database-0.7.9}/transient/README.md +0 -0
@@ -174,3 +174,4 @@ perf-counter.txt
174
174
 
175
175
  /transient/*
176
176
  !/transient/README.md
177
+ transient.db
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlite_database
3
- Version: 0.7.7
3
+ Version: 0.7.9
4
4
  Summary: A weird wrapper for SQLite Connection
5
5
  Home-page: https://github.com/RimuEirnarn/sqlite_database
6
6
  Author: RimuEirnarn
@@ -79,6 +79,29 @@ You can read why this library exists by reading [the history](History.md). The p
79
79
 
80
80
  You can submit any issue if you found a good issue. You can submit a pull request as long as the thing you want complies with what this project aims for.
81
81
 
82
+ ## Development
83
+
84
+ When pulling this repository with latest commit, make sure to install all `dev-requirements.txt` depending on your intentions. It's mostly pylint and pytest.
85
+
86
+ When using `pytest`, do this: `pytest --config-file=./dev-config/pytest.ini` or check any check scripts in `bin/`
87
+
88
+ ### How to install?
89
+
90
+ ```sh
91
+ git clone https://github.com/RimuEirnarn/sqlite_database
92
+ cd sqlite_database
93
+
94
+ python -m venv .venv
95
+ .venv/bin/activate
96
+
97
+ pip install -r ./dev-requirements.txt
98
+ ./bin/check.sh
99
+
100
+ # Above script is equivalent to:
101
+ pylint --rcfile ./dev-config/pylint.toml sqlite_database
102
+ pytest --config-file ./dev-config/pytest.ini
103
+ ```
104
+
82
105
  ## License
83
106
 
84
107
  This library/wrapper/repo/project is licensed with/in BSD 3-Clause "New" or "Revised" License.
@@ -53,6 +53,29 @@ You can read why this library exists by reading [the history](History.md). The p
53
53
 
54
54
  You can submit any issue if you found a good issue. You can submit a pull request as long as the thing you want complies with what this project aims for.
55
55
 
56
+ ## Development
57
+
58
+ When pulling this repository with latest commit, make sure to install all `dev-requirements.txt` depending on your intentions. It's mostly pylint and pytest.
59
+
60
+ When using `pytest`, do this: `pytest --config-file=./dev-config/pytest.ini` or check any check scripts in `bin/`
61
+
62
+ ### How to install?
63
+
64
+ ```sh
65
+ git clone https://github.com/RimuEirnarn/sqlite_database
66
+ cd sqlite_database
67
+
68
+ python -m venv .venv
69
+ .venv/bin/activate
70
+
71
+ pip install -r ./dev-requirements.txt
72
+ ./bin/check.sh
73
+
74
+ # Above script is equivalent to:
75
+ pylint --rcfile ./dev-config/pylint.toml sqlite_database
76
+ pytest --config-file ./dev-config/pytest.ini
77
+ ```
78
+
56
79
  ## License
57
80
 
58
81
  This library/wrapper/repo/project is licensed with/in BSD 3-Clause "New" or "Revised" License.
@@ -9,11 +9,11 @@
9
9
  What API should we bring? These todo's is for simplification; you can do anything with `Database.sql` property
10
10
 
11
11
  - [x] sqlite function support
12
- - [ ] sqlite aggregate function support
13
- - [ ] sqlite window function support
14
- - [ ] sqlite collation support
12
+ - [x] sqlite aggregate function support
13
+ - [x] sqlite window function support
14
+ - [x] sqlite collation support
15
15
  - [x] sqlite pragma
16
- - [ ] sqlite branching subquery (??)
16
+ - [x] sqlite branching subquery (??)
17
17
  - [ ] sqlite 'as' keyword (??)
18
18
  - [x] sqlite select data crunching[^1] by using Database config option (crunch=True)
19
19
  - [x] sqlite select 'only' data. Instead of using `select *`, we should also have `only`. Bring up few data than select everything.[^2]
@@ -25,7 +25,7 @@ The functionality here is outside of sqlite features such as export and import.
25
25
 
26
26
  - [ ] YAML/JSON/TOML/CUSTOM scheme/table include
27
27
  - [x] CSV Export
28
- - [ ] CSV Import
28
+ - [x] CSV Import
29
29
 
30
30
  1. abc
31
31
  - a
@@ -1,5 +1,6 @@
1
1
  """Database"""
2
2
 
3
+ from . import models
3
4
  from .models import BaseModel, model, Foreign, Primary, Unique, hook, validate
4
5
  from .database import Database
5
6
  from ._utils import Row, Null
@@ -14,7 +15,7 @@ def test_installed():
14
15
  return True
15
16
 
16
17
 
17
- __version__ = "0.7.7"
18
+ __version__ = "0.7.9"
18
19
  __all__ = [
19
20
  "Database",
20
21
  "Table",
@@ -139,6 +139,13 @@ class WithCursor(Cursor):
139
139
  def __repr__(self) -> str:
140
140
  return type(self).__name__
141
141
 
142
+ class NoopResource:
143
+ """No-op resource control"""
144
+ def close(self):
145
+ """Close this resource"""
146
+
147
+ def open(self, *_, **__):
148
+ """open something"""
142
149
 
143
150
 
144
151
  def test_installed():
@@ -162,4 +169,5 @@ __all__ = [
162
169
  "AttrDict",
163
170
  "NullObject",
164
171
  "sqlite_multithread_check",
172
+ "NoopResource"
165
173
  ]
@@ -1,6 +1,6 @@
1
1
  """CSV module, used to export database/table to csv"""
2
2
 
3
- from csv import DictWriter
3
+ from csv import DictWriter, DictReader
4
4
  from io import StringIO
5
5
  from os import mkdir
6
6
  from os.path import join as join_path, isfile, exists
@@ -65,3 +65,15 @@ def to_csv_file(table_or_database: Table | Database, file: str):
65
65
  with open(file, "w", encoding="utf-8") as fio:
66
66
  fio.write(readed)
67
67
  return True
68
+
69
+ def from_csv_string(table: Table, data: str):
70
+ """Insert from CSV data"""
71
+ reader = DictReader(data)
72
+ for entry in reader:
73
+ table.insert(entry)
74
+
75
+ def from_csv_file(table: Table, data: str):
76
+ """Insert from CSV file"""
77
+ with open(data, encoding='utf-8') as f:
78
+ fdata = f.read()
79
+ return from_csv_string(table, fdata)
@@ -1,8 +1,7 @@
1
1
  """SQLite Database"""
2
2
 
3
3
  from atexit import register as finalize
4
- from sqlite3 import OperationalError, connect, Connection
5
- from threading import local
4
+ from sqlite3 import OperationalError, connect
6
5
  from typing import Iterable, Literal, Optional
7
6
 
8
7
  from sqlite_database._debug import if_debug_print
@@ -46,6 +45,7 @@ class Database: # pylint: disable=too-many-instance-attributes
46
45
  del kwargs['strict']
47
46
  self._config = None
48
47
  self._closed = False
48
+ self._table_class = Table
49
49
  if not self._closed or self.__dict__.get("_initiated", False) is False:
50
50
  finalize(self._finalizer)
51
51
  self._initiated = True
@@ -119,7 +119,7 @@ class Database: # pylint: disable=too-many-instance-attributes
119
119
  raise DatabaseMissingError(f"table {table} does not exists.")
120
120
 
121
121
  try:
122
- this_table = Table(self, table, __columns)
122
+ this_table = self._table_class(self, table, __columns)
123
123
  except OperationalError as exc:
124
124
  dberror = DatabaseMissingError(f"table {table} does not exists")
125
125
  dberror.add_note(f"{type(exc).__name__}: {exc!s}")
@@ -228,30 +228,3 @@ class Database: # pylint: disable=too-many-instance-attributes
228
228
  def sql(self):
229
229
  """SQL Connection"""
230
230
  return self._database
231
-
232
- class AsyncDatabase(Database):
233
- """Async (threads, subprocess) ready"""
234
-
235
- def __init__(self, path: str, **kwargs) -> None:
236
- super().__init__(path, **kwargs)
237
- self._local = local()
238
-
239
- def _create_connection(self):
240
- conn = getattr(self._local, "conn", None)
241
- if conn is None:
242
- timeout = self._kwargs.pop('timeout', 30)
243
- conn = connect(
244
- self._path,
245
- timeout=timeout,
246
- isolation_level=self._kwargs.pop("isolation_level", None),
247
- check_same_thread=self._kwargs.pop("check_same_thread", False)
248
- )
249
- conn.row_factory = dict_factory
250
- conn.execute("PRAGMA journal_mode=WAL;")
251
- if isinstance(timeout, int):
252
- conn.execute(f'PRAGMA busy_timeout={timeout * 1000};')
253
- self._local.conn = conn
254
-
255
- @property
256
- def _database(self) -> Connection:
257
- return self._local.conn
@@ -1,5 +1,7 @@
1
1
  """Errors"""
2
2
 
3
+ class Rejection(RuntimeError):
4
+ """Cannot push into worker db"""
3
5
 
4
6
  class DependencyError(ImportError):
5
7
  """Specific dependency is missing"""
@@ -30,3 +32,12 @@ class ObjectRemovedError(BaseException):
30
32
 
31
33
  class CuteDemonLordException(Exception):
32
34
  """A demon lord (cute one, somehow a dude) aborts an SQL statement"""
35
+
36
+ class FeatureGatekeep(ValueError):
37
+ """A feature is gatekeeped"""
38
+
39
+ class VersionError(FeatureGatekeep):
40
+ """Current version cannot use this feature"""
41
+
42
+ class ImplementationWarning(FutureWarning):
43
+ """The implementation of specific feature is not supported"""
@@ -363,9 +363,10 @@ class BaseModel: # pylint: disable=too-few-public-methods,too-many-public-metho
363
363
  f"with {self.__class__.__name__}"
364
364
  )
365
365
 
366
- def get_table(self):
366
+ @classmethod
367
+ def get_table(cls):
367
368
  """Return table instance"""
368
- return self._tbl
369
+ return cls._tbl
369
370
 
370
371
  @classmethod
371
372
  def where(cls, **kwargs):
@@ -1,8 +1,9 @@
1
1
  """Table"""
2
2
 
3
- # pylint: disable=too-many-arguments,too-many-public-methods
3
+ # pylint: disable=too-many-arguments,too-many-public-methods,R0801
4
4
 
5
- from sqlite3 import Connection, OperationalError
5
+ from contextvars import ContextVar
6
+ from sqlite3 import Connection, Error, OperationalError
6
7
  from typing import (
7
8
  Any,
8
9
  Generator,
@@ -44,6 +45,7 @@ from .typings import (
44
45
 
45
46
  # Let's add a little bit of 'black' magic here.
46
47
  _null = Function("__NULL__")()
48
+ _tx_stack = ContextVar("_tx_stack", default=[])
47
49
 
48
50
  class Table: # pylint: disable=too-many-instance-attributes
49
51
  """Table. Make sure you remember how the table goes."""
@@ -74,20 +76,40 @@ class Table: # pylint: disable=too-many-instance-attributes
74
76
  if (self._columns is None and aggresive_select) and table != "sqlite_master":
75
77
  self._fetch_columns()
76
78
 
79
+ # def __enter__(self):
80
+ # self._prev_auto = self._auto
81
+ # self._prev_autocommit = self._sql.isolation_level
82
+
83
+ # self._sql.isolation_level = None
84
+ # self._auto = False
85
+ # self._sql.execute("BEGIN TRANSACTION")
86
+ # return self
87
+
88
+ # def __exit__(self, exc_type, _, __):
89
+ # if exc_type is None:
90
+ # self._sql.commit()
91
+ # else:
92
+ # self._sql.rollback()
93
+ # self._sql.isolation_level = self._prev_autocommit
94
+ # self._auto = self._prev_auto
95
+
77
96
  def __enter__(self):
78
97
  self._prev_auto = self._auto
79
98
  self._prev_autocommit = self._sql.isolation_level
80
99
 
81
- self._sql.isolation_level = None
82
100
  self._auto = False
83
- self._sql.execute("BEGIN TRANSACTION")
101
+ self._sql.isolation_level = None
102
+ self._begin_transaction()
84
103
  return self
85
104
 
86
105
  def __exit__(self, exc_type, _, __):
87
106
  if exc_type is None:
88
- self._sql.commit()
107
+ self._commit_transaction()
108
+ self._dirty = False
89
109
  else:
90
- self._sql.rollback()
110
+ self._rollback_transaction()
111
+ self._dirty = False
112
+
91
113
  self._sql.isolation_level = self._prev_autocommit
92
114
  self._auto = self._prev_auto
93
115
 
@@ -167,10 +189,11 @@ class Table: # pylint: disable=too-many-instance-attributes
167
189
  fn = cursor.execute if which == "execute" else cursor.executemany
168
190
  try:
169
191
  fn(query, data)
170
- except OperationalError as exc:
192
+ except Error as exc:
171
193
  if str(exc).startswith("no such table:"):
172
194
  raise TableRemovedError(f"Table {self._table} doesn't exists anymore") from None
173
195
  exc.add_note(f"SQL query: {query}")
196
+ exc.add_note(f"Arguments: {data}")
174
197
  exc.add_note(
175
198
  f"There's about {1 if isinstance(data, dict) else len(data)} value(s) inserted"
176
199
  )
@@ -587,27 +610,61 @@ constraint is enabled."
587
610
  def commit(self):
588
611
  """Commit changes"""
589
612
  self._sql.commit()
613
+ self._dirty = False
590
614
 
591
615
  def rollback(self):
592
616
  """Rollback"""
593
617
  self._sql.rollback()
594
618
  self._dirty = False
595
619
 
620
+ def _begin_transaction(self):
621
+ """Start a transaction or savepoint depending on depth."""
622
+ stack = list(_tx_stack.get()) # copy since ContextVar values are immutable
623
+ depth = len(stack)
624
+
625
+ if depth == 0:
626
+ self._sql.execute("BEGIN TRANSACTION")
627
+ else:
628
+ savepoint_name = f"sp_{depth}"
629
+ self._sql.execute(f"SAVEPOINT {savepoint_name}")
630
+
631
+ stack.append(True)
632
+ _tx_stack.set(stack)
633
+
634
+ def _commit_transaction(self):
635
+ """Commit or release savepoint depending on depth."""
636
+ stack = list(_tx_stack.get())
637
+ depth = len(stack)
638
+
639
+ if depth == 1:
640
+ self._sql.commit()
641
+ elif depth > 1:
642
+ savepoint_name = f"sp_{depth-1}"
643
+ self._sql.execute(f"RELEASE SAVEPOINT {savepoint_name}")
644
+ stack.pop()
645
+ _tx_stack.set(stack)
646
+
647
+ def _rollback_transaction(self):
648
+ """Rollback or rollback to savepoint depending on depth."""
649
+ stack = list(_tx_stack.get())
650
+ depth = len(stack)
651
+
652
+ if depth == 1:
653
+ self._sql.rollback()
654
+ elif depth > 1:
655
+ savepoint_name = f"sp_{depth-1}"
656
+ self._sql.execute(f"ROLLBACK TO SAVEPOINT {savepoint_name}"
657
+ )
658
+
659
+ stack.pop()
660
+ _tx_stack.set(stack)
661
+
596
662
  def count(self):
597
663
  """Count how much objects/rows stored in this table"""
598
664
  # ? Might as well uses __len__? But it's quite expensive.
599
665
  return self.select(what=count("*"))
600
666
 
601
667
  def __repr__(self) -> str:
602
- return f"<Table({self._table}) -> {self._parent_repr}>"
603
-
604
- class AsyncTable(Table):
605
- """Async (threads, subprocess) ready"""
606
-
607
- def __enter__(self):
608
- return self
609
-
610
- def __exit__(self, exc_type, _, __):
611
- pass
668
+ return f"<{type(self).__name__}({self._table}) -> {self._parent_repr}>"
612
669
 
613
670
  __all__ = ["Table"]
@@ -0,0 +1,5 @@
1
+ """Workers"""
2
+
3
+ from .database import DatabaseWorker
4
+
5
+ __all__ = ['DatabaseWorker']
@@ -0,0 +1,214 @@
1
+ """Worker Connection"""
2
+
3
+ # pylint: disable=ungrouped-imports,possibly-used-before-assignment,too-few-public-methods,no-name-in-module,no-member
4
+
5
+ # import os
6
+ from sys import version_info
7
+ from concurrent.futures import Future
8
+ from queue import Empty, Queue
9
+ from sqlite3 import Connection, Cursor
10
+ # from multiprocessing import Event as EventProcess, Process, freeze_support
11
+ from threading import Thread, Event as EventThread
12
+ from typing import Any, Literal, TypeAlias
13
+ from atexit import register as finalize
14
+ import warnings
15
+
16
+ from ..errors import Rejection, ImplementationWarning
17
+
18
+ WorkerType: TypeAlias = Literal["thread"] | Literal["process"]
19
+ FEATURE_MIN_VERSION = (3, 13)
20
+ POSSIBLE_STACKTRACE_COUNT = 6
21
+
22
+ if version_info > FEATURE_MIN_VERSION:
23
+ from queue import ShutDown # type: ignore
24
+ else:
25
+
26
+ class ShutDown(RuntimeError):
27
+ """Raised when put/get with shut-down queue."""
28
+
29
+
30
+ def is_shutdown(worker: "Worker"):
31
+ """Is shutdown?"""
32
+ if worker.is_closed:
33
+ return lambda *a, **kw: None
34
+ return lambda *a, **kw: worker.push(worker.conn.close, "connection", *a, **kw)
35
+
36
+
37
+ class Worker:
38
+ """Worker"""
39
+
40
+ def __init__(self, *args, worker_type: WorkerType = "thread", **kwargs):
41
+ self.conn = Connection(*args, **kwargs)
42
+ self.queue = Queue()
43
+ self.name = f"WorkerDB[{worker_type}]"
44
+ self.daemon = False
45
+ if worker_type in ("thread", "process"):
46
+ self.event = EventThread()
47
+ self.worker = Thread(target=self._run, name=self.name, daemon=self.daemon)
48
+ # elif worker_type == "process":
49
+ # freeze_support()
50
+ # self.event = EventProcess()
51
+ # self.worker = Process(target=self._run, name=self.name, daemon=self.daemon)
52
+ if worker_type == 'process':
53
+ warnings.warn(
54
+ "Worker Process implementation is not supported, reverting back to threading.",
55
+ ImplementationWarning,
56
+ POSSIBLE_STACKTRACE_COUNT)
57
+ self.accepting = self.event
58
+ self._closing = False
59
+ self.event.set()
60
+ self.worker.start()
61
+ finalize(self.close)
62
+
63
+ def recall(self):
64
+ """Recall remainding queues"""
65
+ while True:
66
+ try:
67
+ fn, _, args, kwargs, fut = self.queue.get_nowait()
68
+ except Empty:
69
+ break
70
+ try:
71
+ if callable(fn):
72
+ res = fn(*args, **kwargs)
73
+ fut.set_result(res)
74
+ else:
75
+ fut.set_result(None)
76
+ except Exception as e: # pylint: disable=broad-exception-caught
77
+ fut.set_exception(e)
78
+ finally:
79
+ self.queue.task_done()
80
+
81
+ def _run(self):
82
+ while True:
83
+ try:
84
+ fn, owner, args, kwargs, fut = self.queue.get(timeout=0.01)
85
+ except Empty:
86
+ if self._closing:
87
+ break
88
+ continue
89
+ except ShutDown:
90
+ break
91
+
92
+ # If close signal is received, finish all tasks and quit
93
+ if fn is None or (
94
+ getattr(fn, "__name__", "") == "close" and owner == "connection"
95
+ ):
96
+ fut.set_result(None)
97
+ self.queue.task_done()
98
+ self._closing = True
99
+ self.event.clear()
100
+ self.recall() # Finish all remaining tasks
101
+ break
102
+
103
+ try:
104
+ if callable(fn):
105
+ res = fn(*args, **kwargs)
106
+ fut.set_result(res)
107
+ else:
108
+ setattr(self.conn, fn, kwargs["value"])
109
+ fut.set_result(None)
110
+ except Exception as e: # pylint: disable=broad-exception-caught
111
+ fut.set_exception(e)
112
+ finally:
113
+ self.queue.task_done()
114
+ # Ensure connection is closed
115
+ self.conn.close()
116
+
117
+ def push(self, fn, owner: str, *args, **kwargs):
118
+ """Push to worker"""
119
+ if not self.accepting.is_set() or self._closing:
120
+ exc = Rejection("Cannot push during shutdown")
121
+ exc.add_note(f"Caller: {fn}")
122
+ raise exc
123
+ fut = Future()
124
+ try:
125
+ self.queue.put((fn, owner, args, kwargs, fut))
126
+ except ShutDown:
127
+ fut.set_result(None)
128
+ return fut.result()
129
+ return fut.result() # blocks until worker finishes
130
+
131
+ @property
132
+ def is_closed(self):
133
+ """Return true if worker is closed"""
134
+ return self._closing
135
+
136
+ def close(self, push=True):
137
+ """Close this worker"""
138
+ if self._closing:
139
+ return
140
+ self._closing = True
141
+ self.event.clear()
142
+ if push:
143
+ # Push a close signal to the queue
144
+ fut = Future()
145
+ self.queue.put((lambda: None, "connection", (), {}, fut))
146
+ fut.result()
147
+ self.worker.join()
148
+
149
+ def join(self, timeout: float = 0):
150
+ """Join this worker"""
151
+ self.worker.join(timeout)
152
+
153
+
154
+ class WorkerCursor:
155
+ """Worker cursor"""
156
+
157
+ __slots__ = ("_worker", "_cursor")
158
+
159
+ def __init__(self, worker: Worker, real_cursor: Cursor):
160
+ self._worker: Worker = worker
161
+ self._cursor: Cursor = real_cursor
162
+
163
+ def __getattr__(self, item):
164
+ # print(self, item)
165
+ if item in self.__slots__:
166
+ return super().__getattribute__(item)
167
+
168
+ attr = getattr(self._cursor, item)
169
+ if callable(attr):
170
+ return lambda *a, **kw: self._worker.push(attr, "cursor", *a, **kw)
171
+ return attr
172
+
173
+
174
+ class WorkerConnection:
175
+ """Worker connection"""
176
+
177
+ def __init__(self, *args, worker_type: WorkerType = "thread", **kwargs):
178
+ self._real: Worker = Worker(*args, worker_type=worker_type, **kwargs)
179
+
180
+ def __getattr__(self, item):
181
+ # print(self, item)
182
+ if item in ("_real", "cursor"):
183
+ return super().__getattribute__(item)
184
+
185
+ if item in ("join",):
186
+ return getattr(self._real, item)
187
+
188
+ conn = self._real.conn
189
+ attr = getattr(conn, item)
190
+ if item == "close":
191
+ return is_shutdown(self._real)
192
+
193
+ if callable(attr):
194
+ return lambda *a, **kw: self._real.push(attr, "connection", *a, **kw)
195
+ return attr
196
+
197
+ def cursor(self, *args, **kwargs):
198
+ """Return cursor object"""
199
+ real = self._real.push(
200
+ getattr(self._real.conn, "cursor"), "connection", *args, **kwargs
201
+ )
202
+ return WorkerCursor(self._real, real)
203
+
204
+ def __setattr__(self, name: str, value: Any) -> None:
205
+ if name == "_real":
206
+ super().__setattr__(name, value)
207
+ return
208
+ self._real.push(name, "connection", value=value)
209
+
210
+ def __enter__(self):
211
+ return self.cursor()
212
+
213
+ def __exit__(self, _, exc, __):
214
+ pass
@@ -0,0 +1,62 @@
1
+ """Database Worker"""
2
+
3
+ import warnings
4
+
5
+ from sqlite_database._utils import dict_factory, NoopResource
6
+ from sqlite_database.database import Database
7
+ from sqlite_database.workers.connection import WorkerConnection, WorkerType
8
+ from sqlite_database.errors import VersionError
9
+
10
+
11
+ class DatabaseWorker(Database):
12
+ """Database Worker"""
13
+
14
+ def __init__(self, path: str, worker_type: WorkerType = "thread", **kwargs) -> None:
15
+ self._worker_type: WorkerType = worker_type
16
+ super().__init__(path, **kwargs)
17
+
18
+ def _create_connection(self):
19
+ timeout = self._kwargs.pop("timeout", 30)
20
+ if not isinstance(timeout, int):
21
+ timeout = 30
22
+ # conn = connect(
23
+ # self._path,
24
+ # timeout=timeout,
25
+ # isolation_level=self._kwargs.pop("isolation_level", None),
26
+ # check_same_thread=self._kwargs.pop("check_same_thread", False)
27
+ # )
28
+ # conn.row_factory = dict_factory
29
+ try:
30
+ self._database = WorkerConnection(
31
+ self._path,
32
+ worker_type=self._worker_type,
33
+ timeout=timeout,
34
+ isolation_level=self._kwargs.pop("isolation_level", None),
35
+ check_same_thread=self._kwargs.pop("check_same_thread", False)
36
+ )
37
+ self._database.row_factory = dict_factory
38
+ self._database.execute("PRAGMA journal_mode=WAL;")
39
+ self._database.execute(f'PRAGMA busy_timeout={timeout * 1000};')
40
+ except VersionError:
41
+ self._database = NoopResource()
42
+ raise
43
+
44
+ def close(self):
45
+ self._database.close()
46
+ self._database.join() # type: ignore
47
+
48
+ def join(self):
49
+ """Join the worker thread/process"""
50
+ self._database.join() # type: ignore
51
+
52
+ def __del__(self):
53
+ warnings.warn(
54
+ (f"Instance of {self} was not properly closed." # type: ignore
55
+ "The process is threathened to be stalled indefinitely"
56
+ ),
57
+ ResourceWarning,
58
+ stacklevel=2
59
+ )
60
+
61
+ def __repr__(self) -> str:
62
+ return f"<{type(self).__name__} {self._database._real.name}>" # type: ignore
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlite_database
3
- Version: 0.7.7
3
+ Version: 0.7.9
4
4
  Summary: A weird wrapper for SQLite Connection
5
5
  Home-page: https://github.com/RimuEirnarn/sqlite_database
6
6
  Author: RimuEirnarn
@@ -79,6 +79,29 @@ You can read why this library exists by reading [the history](History.md). The p
79
79
 
80
80
  You can submit any issue if you found a good issue. You can submit a pull request as long as the thing you want complies with what this project aims for.
81
81
 
82
+ ## Development
83
+
84
+ When pulling this repository with latest commit, make sure to install all `dev-requirements.txt` depending on your intentions. It's mostly pylint and pytest.
85
+
86
+ When using `pytest`, do this: `pytest --config-file=./dev-config/pytest.ini` or check any check scripts in `bin/`
87
+
88
+ ### How to install?
89
+
90
+ ```sh
91
+ git clone https://github.com/RimuEirnarn/sqlite_database
92
+ cd sqlite_database
93
+
94
+ python -m venv .venv
95
+ .venv/bin/activate
96
+
97
+ pip install -r ./dev-requirements.txt
98
+ ./bin/check.sh
99
+
100
+ # Above script is equivalent to:
101
+ pylint --rcfile ./dev-config/pylint.toml sqlite_database
102
+ pytest --config-file ./dev-config/pytest.ini
103
+ ```
104
+
82
105
  ## License
83
106
 
84
107
  This library/wrapper/repo/project is licensed with/in BSD 3-Clause "New" or "Revised" License.
@@ -88,6 +88,9 @@ sqlite_database/models/helpers.py
88
88
  sqlite_database/models/mixin.py
89
89
  sqlite_database/models/query_builder.py
90
90
  sqlite_database/models/type_checkers.py
91
+ sqlite_database/workers/__init__.py
92
+ sqlite_database/workers/connection.py
93
+ sqlite_database/workers/database.py
91
94
  tests/__init__.py
92
95
  tests/manual_test_performances.py
93
96
  tests/test_internals.py
@@ -2,9 +2,11 @@
2
2
 
3
3
  from sqlite3 import OperationalError
4
4
  from random import randint
5
+ import sys
5
6
 
6
- from pytest import raises
7
+ from pytest import mark, raises
7
8
  from sqlite_database import Database, integer
9
+ from sqlite_database.workers import DatabaseWorker
8
10
 
9
11
  from ..setup import setup_database_fns, setup_database, count
10
12
 
@@ -63,3 +65,14 @@ def test_vacuum():
63
65
  _ = [t.delete_one({"a": randint(0, 1000)})]
64
66
  t.commit()
65
67
  db.vacuum()
68
+
69
+ # @mark.skipif(sys.version_info < (3, 13), reason="Worker feature is exclusive in 3.13")
70
+ def test_worker():
71
+ """Test worker"""
72
+
73
+ db = DatabaseWorker(":memory:")
74
+ t = db.create_table("t", [integer('a')])
75
+ t.insert({'a': 1})
76
+ t.commit()
77
+ assert t.select_one({'a': 1}) is not None
78
+ db.close()
File without changes