sa-values 0.1.0__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.
- sa_values-0.1.0/PKG-INFO +148 -0
- sa_values-0.1.0/README.md +132 -0
- sa_values-0.1.0/pyproject.toml +41 -0
- sa_values-0.1.0/setup.cfg +4 -0
- sa_values-0.1.0/src/sa_values/__init__.py +32 -0
- sa_values-0.1.0/src/sa_values/setup.py +66 -0
- sa_values-0.1.0/src/sa_values/table.py +56 -0
- sa_values-0.1.0/src/sa_values/values.py +178 -0
- sa_values-0.1.0/src/sa_values.egg-info/PKG-INFO +148 -0
- sa_values-0.1.0/src/sa_values.egg-info/SOURCES.txt +11 -0
- sa_values-0.1.0/src/sa_values.egg-info/dependency_links.txt +1 -0
- sa_values-0.1.0/src/sa_values.egg-info/requires.txt +9 -0
- sa_values-0.1.0/src/sa_values.egg-info/top_level.txt +1 -0
sa_values-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sa-values
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Storing key-value pairs in database powered by SQLAlchemy.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Keywords: sqlalchemy,key-value,database,configuration
|
|
7
|
+
Requires-Python: >=3.12
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: sqlalchemy
|
|
10
|
+
Provides-Extra: test
|
|
11
|
+
Requires-Dist: pytest; extra == "test"
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: sa-values[test]; extra == "dev"
|
|
14
|
+
Requires-Dist: build; extra == "dev"
|
|
15
|
+
Requires-Dist: twine; extra == "dev"
|
|
16
|
+
|
|
17
|
+
[](https://github.com/elmordo/sa-values/actions/workflows/tests.yml)
|
|
18
|
+
|
|
19
|
+
The `sa-values` (SQLAlchemy values) is designed as light-weight library for storing key-value pairs in database.
|
|
20
|
+
|
|
21
|
+
# Installation
|
|
22
|
+
|
|
23
|
+
```shell
|
|
24
|
+
pip install sa-values
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
# Quick start
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from sqlalchemy import create_engine
|
|
31
|
+
from sa_values import setup_sa_values, SaValues
|
|
32
|
+
|
|
33
|
+
# connect to db and setup sa-values
|
|
34
|
+
conn = create_engine("sqlite:///:memory:").connect()
|
|
35
|
+
setup_sa_values(conn)
|
|
36
|
+
|
|
37
|
+
sa_vals = SaValues(conn)
|
|
38
|
+
sa_vals.set("key", "value")
|
|
39
|
+
print("Stored value: ", sa_vals.get("key"))
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
# Supported DBs
|
|
43
|
+
|
|
44
|
+
The only officially supported database system is `sqlite`. Other DB systems should work too (experimentally tested on
|
|
45
|
+
PostgreSQL), but use it at your own risk.
|
|
46
|
+
|
|
47
|
+
# Usage
|
|
48
|
+
|
|
49
|
+
The `sa-values` is based on SQLAlchemy and provides a simple interface for storing and retrieving key-value pairs in a
|
|
50
|
+
database.
|
|
51
|
+
|
|
52
|
+
## Initialization
|
|
53
|
+
|
|
54
|
+
The `sa-values` use a single table to store all data. Default table name is `sa_values`. Before any operation is
|
|
55
|
+
performed, the initialization should be done.
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from sqlalchemy import create_engine
|
|
59
|
+
from sa_values import setup_sa_values
|
|
60
|
+
|
|
61
|
+
# connect to db and setup sa-values
|
|
62
|
+
conn = create_engine("sqlite:///:memory:").connect()
|
|
63
|
+
setup_sa_values(conn)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The `setup_sa_values` function accepts the second, optional, parameter `value_table_name`. This can be used to specify a
|
|
67
|
+
different table name for storing key-value pairs.
|
|
68
|
+
|
|
69
|
+
## Single values
|
|
70
|
+
|
|
71
|
+
When using single values, each key can be stored only once. When setting an exising key, the original value is
|
|
72
|
+
overridden.
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from sqlalchemy import create_engine
|
|
76
|
+
from sa_values import setup_sa_values, SaValues
|
|
77
|
+
|
|
78
|
+
# connect to db and setup sa-values
|
|
79
|
+
conn = create_engine("sqlite:///:memory:").connect()
|
|
80
|
+
setup_sa_values(conn)
|
|
81
|
+
values = SaValues(conn)
|
|
82
|
+
|
|
83
|
+
if not values.has("some_value"):
|
|
84
|
+
print("Some_value is not stored")
|
|
85
|
+
values.set("some_value", "123")
|
|
86
|
+
if values.has("some_value"):
|
|
87
|
+
print("Some_value is stored")
|
|
88
|
+
print("The value is: ", values.get("some_value"))
|
|
89
|
+
values.set("some_value", "456")
|
|
90
|
+
print("New value is: ", values.get("some_value"))
|
|
91
|
+
|
|
92
|
+
values.delete("some_value")
|
|
93
|
+
if not values.has("some_value"):
|
|
94
|
+
print("Some_value is not stored anymore")
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Multiple values
|
|
99
|
+
|
|
100
|
+
If you want to manage key with more than one value, you can use the `SaValues::multi_value_key(key)`. The method returns
|
|
101
|
+
an object for the multi-value key management. Each value must be unique. In case of duplicate values, the second and
|
|
102
|
+
following values are ignored.
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from sqlalchemy import create_engine
|
|
106
|
+
from sa_values import setup_sa_values, SaValues
|
|
107
|
+
|
|
108
|
+
# connect to db and setup sa-values
|
|
109
|
+
conn = create_engine("sqlite:///:memory:").connect()
|
|
110
|
+
setup_sa_values(conn)
|
|
111
|
+
values = SaValues(conn)
|
|
112
|
+
|
|
113
|
+
multi_key = values.multi_value_key("multi_key")
|
|
114
|
+
|
|
115
|
+
multi_key.add("value1")
|
|
116
|
+
multi_key.add("value2")
|
|
117
|
+
multi_key.add("value2") # do nothing - each value can be stored only once
|
|
118
|
+
multi_key.add("value3")
|
|
119
|
+
|
|
120
|
+
print("Values: ", multi_key.get_all()) # ["value1", "value2", "value3"]
|
|
121
|
+
|
|
122
|
+
multi_key.delete("value2")
|
|
123
|
+
|
|
124
|
+
print("Values: ", multi_key.get()) # ["value1", "value3"]
|
|
125
|
+
|
|
126
|
+
print("value1 exists: ", multi_key.has("value1")) # true
|
|
127
|
+
print("value10 exists: ", multi_key.has("value10")) # false
|
|
128
|
+
|
|
129
|
+
multi_key.clear() # clear all
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Teardown
|
|
133
|
+
|
|
134
|
+
When the `sa-values` is not needed anymore, it can be tear downed by calling the `teardown_sa_values`
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
from sqlalchemy import create_engine
|
|
138
|
+
from sa_values import SaValues, teardown_sa_values
|
|
139
|
+
|
|
140
|
+
# connect to db and setup sa-values
|
|
141
|
+
conn = create_engine("sqlite:///:memory:").connect()
|
|
142
|
+
teardown_sa_values(conn)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
# Buy me a ~~coffee~~ beer
|
|
146
|
+
|
|
147
|
+
If you like this library, or you want to support its development, support me by one
|
|
148
|
+
cold [beer](https://www.buymeacoffee.com/elmordo). The beer is tasty and full of vitamins :-)
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
[](https://github.com/elmordo/sa-values/actions/workflows/tests.yml)
|
|
2
|
+
|
|
3
|
+
The `sa-values` (SQLAlchemy values) is designed as light-weight library for storing key-value pairs in database.
|
|
4
|
+
|
|
5
|
+
# Installation
|
|
6
|
+
|
|
7
|
+
```shell
|
|
8
|
+
pip install sa-values
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
# Quick start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from sqlalchemy import create_engine
|
|
15
|
+
from sa_values import setup_sa_values, SaValues
|
|
16
|
+
|
|
17
|
+
# connect to db and setup sa-values
|
|
18
|
+
conn = create_engine("sqlite:///:memory:").connect()
|
|
19
|
+
setup_sa_values(conn)
|
|
20
|
+
|
|
21
|
+
sa_vals = SaValues(conn)
|
|
22
|
+
sa_vals.set("key", "value")
|
|
23
|
+
print("Stored value: ", sa_vals.get("key"))
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
# Supported DBs
|
|
27
|
+
|
|
28
|
+
The only officially supported database system is `sqlite`. Other DB systems should work too (experimentally tested on
|
|
29
|
+
PostgreSQL), but use it at your own risk.
|
|
30
|
+
|
|
31
|
+
# Usage
|
|
32
|
+
|
|
33
|
+
The `sa-values` is based on SQLAlchemy and provides a simple interface for storing and retrieving key-value pairs in a
|
|
34
|
+
database.
|
|
35
|
+
|
|
36
|
+
## Initialization
|
|
37
|
+
|
|
38
|
+
The `sa-values` use a single table to store all data. Default table name is `sa_values`. Before any operation is
|
|
39
|
+
performed, the initialization should be done.
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from sqlalchemy import create_engine
|
|
43
|
+
from sa_values import setup_sa_values
|
|
44
|
+
|
|
45
|
+
# connect to db and setup sa-values
|
|
46
|
+
conn = create_engine("sqlite:///:memory:").connect()
|
|
47
|
+
setup_sa_values(conn)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The `setup_sa_values` function accepts the second, optional, parameter `value_table_name`. This can be used to specify a
|
|
51
|
+
different table name for storing key-value pairs.
|
|
52
|
+
|
|
53
|
+
## Single values
|
|
54
|
+
|
|
55
|
+
When using single values, each key can be stored only once. When setting an exising key, the original value is
|
|
56
|
+
overridden.
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from sqlalchemy import create_engine
|
|
60
|
+
from sa_values import setup_sa_values, SaValues
|
|
61
|
+
|
|
62
|
+
# connect to db and setup sa-values
|
|
63
|
+
conn = create_engine("sqlite:///:memory:").connect()
|
|
64
|
+
setup_sa_values(conn)
|
|
65
|
+
values = SaValues(conn)
|
|
66
|
+
|
|
67
|
+
if not values.has("some_value"):
|
|
68
|
+
print("Some_value is not stored")
|
|
69
|
+
values.set("some_value", "123")
|
|
70
|
+
if values.has("some_value"):
|
|
71
|
+
print("Some_value is stored")
|
|
72
|
+
print("The value is: ", values.get("some_value"))
|
|
73
|
+
values.set("some_value", "456")
|
|
74
|
+
print("New value is: ", values.get("some_value"))
|
|
75
|
+
|
|
76
|
+
values.delete("some_value")
|
|
77
|
+
if not values.has("some_value"):
|
|
78
|
+
print("Some_value is not stored anymore")
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Multiple values
|
|
83
|
+
|
|
84
|
+
If you want to manage key with more than one value, you can use the `SaValues::multi_value_key(key)`. The method returns
|
|
85
|
+
an object for the multi-value key management. Each value must be unique. In case of duplicate values, the second and
|
|
86
|
+
following values are ignored.
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from sqlalchemy import create_engine
|
|
90
|
+
from sa_values import setup_sa_values, SaValues
|
|
91
|
+
|
|
92
|
+
# connect to db and setup sa-values
|
|
93
|
+
conn = create_engine("sqlite:///:memory:").connect()
|
|
94
|
+
setup_sa_values(conn)
|
|
95
|
+
values = SaValues(conn)
|
|
96
|
+
|
|
97
|
+
multi_key = values.multi_value_key("multi_key")
|
|
98
|
+
|
|
99
|
+
multi_key.add("value1")
|
|
100
|
+
multi_key.add("value2")
|
|
101
|
+
multi_key.add("value2") # do nothing - each value can be stored only once
|
|
102
|
+
multi_key.add("value3")
|
|
103
|
+
|
|
104
|
+
print("Values: ", multi_key.get_all()) # ["value1", "value2", "value3"]
|
|
105
|
+
|
|
106
|
+
multi_key.delete("value2")
|
|
107
|
+
|
|
108
|
+
print("Values: ", multi_key.get()) # ["value1", "value3"]
|
|
109
|
+
|
|
110
|
+
print("value1 exists: ", multi_key.has("value1")) # true
|
|
111
|
+
print("value10 exists: ", multi_key.has("value10")) # false
|
|
112
|
+
|
|
113
|
+
multi_key.clear() # clear all
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Teardown
|
|
117
|
+
|
|
118
|
+
When the `sa-values` is not needed anymore, it can be tear downed by calling the `teardown_sa_values`
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
from sqlalchemy import create_engine
|
|
122
|
+
from sa_values import SaValues, teardown_sa_values
|
|
123
|
+
|
|
124
|
+
# connect to db and setup sa-values
|
|
125
|
+
conn = create_engine("sqlite:///:memory:").connect()
|
|
126
|
+
teardown_sa_values(conn)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
# Buy me a ~~coffee~~ beer
|
|
130
|
+
|
|
131
|
+
If you like this library, or you want to support its development, support me by one
|
|
132
|
+
cold [beer](https://www.buymeacoffee.com/elmordo). The beer is tasty and full of vitamins :-)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "sa-values"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Storing key-value pairs in database powered by SQLAlchemy."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
keywords = ["sqlalchemy", "key-value", "database", "configuration"]
|
|
11
|
+
license = "MIT"
|
|
12
|
+
requires-python = ">=3.12"
|
|
13
|
+
dependencies = [
|
|
14
|
+
"sqlalchemy",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.optional-dependencies]
|
|
18
|
+
test = [
|
|
19
|
+
"pytest",
|
|
20
|
+
]
|
|
21
|
+
dev = [
|
|
22
|
+
"sa-values[test]",
|
|
23
|
+
"build",
|
|
24
|
+
"twine",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[tool.setuptools]
|
|
28
|
+
package-dir = { "" = "src" }
|
|
29
|
+
|
|
30
|
+
[tool.setuptools.packages.find]
|
|
31
|
+
where = ["src"]
|
|
32
|
+
|
|
33
|
+
[tool.ruff.lint.isort]
|
|
34
|
+
force-sort-within-sections = true
|
|
35
|
+
order-by-type = false
|
|
36
|
+
lines-after-imports = 2
|
|
37
|
+
|
|
38
|
+
[tool.ruff.format]
|
|
39
|
+
quote-style = "double"
|
|
40
|
+
indent-style = "space"
|
|
41
|
+
docstring-code-format = true
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) [YEAR] [COPYRIGHT HOLDER]
|
|
4
|
+
#
|
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
# furnished to do so, subject to the following conditions:
|
|
11
|
+
#
|
|
12
|
+
# The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
# copies or substantial portions of the Software.
|
|
14
|
+
#
|
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
# SOFTWARE.
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from .setup import setup_sa_values, teardown_sa_values
|
|
25
|
+
from .values import SaValues
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"SaValues",
|
|
30
|
+
"setup_sa_values",
|
|
31
|
+
"teardown_sa_values",
|
|
32
|
+
]
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) [YEAR] [COPYRIGHT HOLDER]
|
|
4
|
+
#
|
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
# furnished to do so, subject to the following conditions:
|
|
11
|
+
#
|
|
12
|
+
# The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
# copies or substantial portions of the Software.
|
|
14
|
+
#
|
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
# SOFTWARE.
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from sqlalchemy import Connection
|
|
25
|
+
from sqlalchemy.sql.ddl import CreateTable, DropTable
|
|
26
|
+
|
|
27
|
+
from sa_values.table import setup_value_table
|
|
28
|
+
|
|
29
|
+
from .table import get_value_table
|
|
30
|
+
from .values import SaValues
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
_TABLE_VERSION = 1
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def setup_sa_values(
|
|
37
|
+
connection: Connection, value_table_name: str | None = None
|
|
38
|
+
) -> None:
|
|
39
|
+
if value_table_name is not None:
|
|
40
|
+
setup_value_table(value_table_name)
|
|
41
|
+
|
|
42
|
+
_create_table(connection)
|
|
43
|
+
|
|
44
|
+
values = SaValues(connection)
|
|
45
|
+
values._allow_empty = True
|
|
46
|
+
|
|
47
|
+
if not values.has(""):
|
|
48
|
+
values.set("", str(_TABLE_VERSION))
|
|
49
|
+
else:
|
|
50
|
+
current_version = int(values.get(""))
|
|
51
|
+
if current_version != _TABLE_VERSION:
|
|
52
|
+
raise ValueError(f"Invalid table version: {current_version}")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def teardown_sa_values(connection: Connection) -> None:
|
|
56
|
+
_drop_table(connection)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _create_table(connection: Connection) -> None:
|
|
60
|
+
create_stmt = CreateTable(get_value_table(), if_not_exists=True)
|
|
61
|
+
connection.execute(create_stmt)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _drop_table(connection: Connection) -> None:
|
|
65
|
+
stmt = DropTable(get_value_table(), if_exists=True)
|
|
66
|
+
connection.execute(stmt)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) [YEAR] [COPYRIGHT HOLDER]
|
|
4
|
+
#
|
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
# furnished to do so, subject to the following conditions:
|
|
11
|
+
#
|
|
12
|
+
# The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
# copies or substantial portions of the Software.
|
|
14
|
+
#
|
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
# SOFTWARE.
|
|
22
|
+
from sqlalchemy import Column, Integer, MetaData, String, Table
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
_metadata = MetaData()
|
|
26
|
+
|
|
27
|
+
_value_table = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_value_table() -> Table:
|
|
31
|
+
"""Get the value table. If the table instance does not exists, create it"""
|
|
32
|
+
global _value_table
|
|
33
|
+
|
|
34
|
+
if _value_table is None:
|
|
35
|
+
_value_table = _create_value_table()
|
|
36
|
+
return _value_table
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def setup_value_table(table_name: str = "sa_values") -> None:
|
|
40
|
+
"""Create the value table with the given name"""
|
|
41
|
+
global _value_table
|
|
42
|
+
_value_table = _create_value_table(table_name)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _create_value_table(table_name: str = "sa_values") -> Table:
|
|
46
|
+
"""Create and return the value table with the given name."""
|
|
47
|
+
if (existing_table := _metadata.tables.get(table_name)) is not None:
|
|
48
|
+
return existing_table
|
|
49
|
+
|
|
50
|
+
return Table(
|
|
51
|
+
table_name,
|
|
52
|
+
_metadata,
|
|
53
|
+
Column("id", Integer, primary_key=True),
|
|
54
|
+
Column("name", String, nullable=False),
|
|
55
|
+
Column("value", String, nullable=False),
|
|
56
|
+
)
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) [YEAR] [COPYRIGHT HOLDER]
|
|
4
|
+
#
|
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
# furnished to do so, subject to the following conditions:
|
|
11
|
+
#
|
|
12
|
+
# The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
# copies or substantial portions of the Software.
|
|
14
|
+
#
|
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
# SOFTWARE.
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from typing import Iterator
|
|
25
|
+
|
|
26
|
+
from sqlalchemy import Connection, delete, insert, select, Table, update
|
|
27
|
+
|
|
28
|
+
from .table import get_value_table
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SaValues:
|
|
32
|
+
"""
|
|
33
|
+
Features:
|
|
34
|
+
|
|
35
|
+
* manage single value keys (get, set, has, delete)
|
|
36
|
+
* manage multi-value keys. The multi-value accessor is provided by calling `multi_value_key` method.
|
|
37
|
+
|
|
38
|
+
The multi-value keys interface is provided by the `MultiValueKey` class.
|
|
39
|
+
|
|
40
|
+
Mutations flush the session; the caller owns the transaction. Keys and values
|
|
41
|
+
are strings, including empty strings.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(self, connection: Connection):
|
|
45
|
+
self.connection: Connection = connection
|
|
46
|
+
self._value_table = get_value_table()
|
|
47
|
+
self._allow_empty = False
|
|
48
|
+
|
|
49
|
+
def get(self, key: str) -> str | None:
|
|
50
|
+
"""Return the oldest value by row ID, or None if the key is absent."""
|
|
51
|
+
stmt = (
|
|
52
|
+
select(self._value_table.c.value)
|
|
53
|
+
.where(self._value_table.c.name == key)
|
|
54
|
+
.order_by(self._value_table.c.id)
|
|
55
|
+
.limit(1)
|
|
56
|
+
)
|
|
57
|
+
return self.connection.scalar(stmt)
|
|
58
|
+
|
|
59
|
+
def get_keys(self) -> list[str]:
|
|
60
|
+
"""Return all keys."""
|
|
61
|
+
stmt = (
|
|
62
|
+
select(self._value_table.c.name)
|
|
63
|
+
.distinct()
|
|
64
|
+
.order_by(self._value_table.c.name)
|
|
65
|
+
)
|
|
66
|
+
return list(self.connection.scalars(stmt))
|
|
67
|
+
|
|
68
|
+
def set(self, key: str, value: str) -> None:
|
|
69
|
+
"""Store exactly one value for the key, replacing any existing values."""
|
|
70
|
+
if not key and not self._allow_empty:
|
|
71
|
+
raise ValueError("key must be non-empty")
|
|
72
|
+
stmt = (
|
|
73
|
+
select(self._value_table.c.id)
|
|
74
|
+
.where(self._value_table.c.name == key)
|
|
75
|
+
.order_by(self._value_table.c.id)
|
|
76
|
+
)
|
|
77
|
+
item_ids = list(self.connection.scalars(stmt))
|
|
78
|
+
if item_ids:
|
|
79
|
+
self.connection.execute(
|
|
80
|
+
update(self._value_table)
|
|
81
|
+
.where(self._value_table.c.id == item_ids[0])
|
|
82
|
+
.values(value=value),
|
|
83
|
+
)
|
|
84
|
+
if len(item_ids) > 1:
|
|
85
|
+
self.connection.execute(
|
|
86
|
+
delete(self._value_table).where(
|
|
87
|
+
self._value_table.c.id.in_(item_ids[1:])
|
|
88
|
+
),
|
|
89
|
+
)
|
|
90
|
+
else:
|
|
91
|
+
self.connection.execute(
|
|
92
|
+
insert(self._value_table).values(name=key, value=value)
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def has(self, key: str) -> bool:
|
|
96
|
+
"""Return whether the key has any stored values."""
|
|
97
|
+
return self.get(key) is not None
|
|
98
|
+
|
|
99
|
+
def delete(self, key: str) -> None:
|
|
100
|
+
"""Remove all values for the key; missing keys are ignored."""
|
|
101
|
+
self.multi_value_key(key).clear()
|
|
102
|
+
|
|
103
|
+
def multi_value_key(self, key: str) -> MultiValueKey:
|
|
104
|
+
"""Return an accessor sharing this session, without creating any rows."""
|
|
105
|
+
return MultiValueKey(self.connection, self._value_table, key)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class MultiValueKey:
|
|
109
|
+
"""Access to multi-value configuration keys.
|
|
110
|
+
|
|
111
|
+
Features:
|
|
112
|
+
|
|
113
|
+
* iterate over values
|
|
114
|
+
* clear all values
|
|
115
|
+
* get all values
|
|
116
|
+
* manage values (get, set, has, delete)
|
|
117
|
+
|
|
118
|
+
Repeated set calls store each value only once for the key. This does not
|
|
119
|
+
guarantee uniqueness across concurrent writers. Reads return distinct values
|
|
120
|
+
in oldest-row order. Mutations flush, leaving transaction control to the caller.
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
def __init__(self, connection: Connection, value_table: Table, key: str):
|
|
124
|
+
if not key:
|
|
125
|
+
raise ValueError("key must be non-empty")
|
|
126
|
+
self.connection = connection
|
|
127
|
+
self.key = key
|
|
128
|
+
self._value_table = value_table
|
|
129
|
+
|
|
130
|
+
def __iter__(self) -> Iterator[str]:
|
|
131
|
+
return iter(self.get_all())
|
|
132
|
+
|
|
133
|
+
def get_all(self) -> list[str]:
|
|
134
|
+
"""Return distinct values in oldest-row order, or an empty list."""
|
|
135
|
+
stmt = (
|
|
136
|
+
select(self._value_table.c.value)
|
|
137
|
+
.where(self._value_table.c.name == self.key)
|
|
138
|
+
.order_by(self._value_table.c.id)
|
|
139
|
+
)
|
|
140
|
+
return list(dict.fromkeys(self.connection.scalars(stmt)))
|
|
141
|
+
|
|
142
|
+
def get(self, value: str) -> str | None:
|
|
143
|
+
"""Return the matching string, or None if it is absent."""
|
|
144
|
+
stmt = (
|
|
145
|
+
select(self._value_table.c.value)
|
|
146
|
+
.where(
|
|
147
|
+
self._value_table.c.name == self.key,
|
|
148
|
+
self._value_table.c.value == value,
|
|
149
|
+
)
|
|
150
|
+
.limit(1)
|
|
151
|
+
)
|
|
152
|
+
return self.connection.scalar(stmt)
|
|
153
|
+
|
|
154
|
+
def has(self, value: str) -> bool:
|
|
155
|
+
"""Return whether this key contains the value."""
|
|
156
|
+
return self.get(value) is not None
|
|
157
|
+
|
|
158
|
+
def add(self, value: str) -> None:
|
|
159
|
+
"""Add the value if absent, leaving other values intact."""
|
|
160
|
+
if not self.has(value):
|
|
161
|
+
self.connection.execute(
|
|
162
|
+
insert(self._value_table).values(name=self.key, value=value)
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
def delete(self, value: str) -> None:
|
|
166
|
+
"""Remove every matching row; missing values are ignored."""
|
|
167
|
+
self.connection.execute(
|
|
168
|
+
delete(self._value_table).where(
|
|
169
|
+
self._value_table.c.name == self.key,
|
|
170
|
+
self._value_table.c.value == value,
|
|
171
|
+
),
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
def clear(self) -> None:
|
|
175
|
+
"""Remove all values for this key; missing keys are ignored."""
|
|
176
|
+
self.connection.execute(
|
|
177
|
+
delete(self._value_table).where(self._value_table.c.name == self.key)
|
|
178
|
+
)
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sa-values
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Storing key-value pairs in database powered by SQLAlchemy.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Keywords: sqlalchemy,key-value,database,configuration
|
|
7
|
+
Requires-Python: >=3.12
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: sqlalchemy
|
|
10
|
+
Provides-Extra: test
|
|
11
|
+
Requires-Dist: pytest; extra == "test"
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: sa-values[test]; extra == "dev"
|
|
14
|
+
Requires-Dist: build; extra == "dev"
|
|
15
|
+
Requires-Dist: twine; extra == "dev"
|
|
16
|
+
|
|
17
|
+
[](https://github.com/elmordo/sa-values/actions/workflows/tests.yml)
|
|
18
|
+
|
|
19
|
+
The `sa-values` (SQLAlchemy values) is designed as light-weight library for storing key-value pairs in database.
|
|
20
|
+
|
|
21
|
+
# Installation
|
|
22
|
+
|
|
23
|
+
```shell
|
|
24
|
+
pip install sa-values
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
# Quick start
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from sqlalchemy import create_engine
|
|
31
|
+
from sa_values import setup_sa_values, SaValues
|
|
32
|
+
|
|
33
|
+
# connect to db and setup sa-values
|
|
34
|
+
conn = create_engine("sqlite:///:memory:").connect()
|
|
35
|
+
setup_sa_values(conn)
|
|
36
|
+
|
|
37
|
+
sa_vals = SaValues(conn)
|
|
38
|
+
sa_vals.set("key", "value")
|
|
39
|
+
print("Stored value: ", sa_vals.get("key"))
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
# Supported DBs
|
|
43
|
+
|
|
44
|
+
The only officially supported database system is `sqlite`. Other DB systems should work too (experimentally tested on
|
|
45
|
+
PostgreSQL), but use it at your own risk.
|
|
46
|
+
|
|
47
|
+
# Usage
|
|
48
|
+
|
|
49
|
+
The `sa-values` is based on SQLAlchemy and provides a simple interface for storing and retrieving key-value pairs in a
|
|
50
|
+
database.
|
|
51
|
+
|
|
52
|
+
## Initialization
|
|
53
|
+
|
|
54
|
+
The `sa-values` use a single table to store all data. Default table name is `sa_values`. Before any operation is
|
|
55
|
+
performed, the initialization should be done.
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from sqlalchemy import create_engine
|
|
59
|
+
from sa_values import setup_sa_values
|
|
60
|
+
|
|
61
|
+
# connect to db and setup sa-values
|
|
62
|
+
conn = create_engine("sqlite:///:memory:").connect()
|
|
63
|
+
setup_sa_values(conn)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The `setup_sa_values` function accepts the second, optional, parameter `value_table_name`. This can be used to specify a
|
|
67
|
+
different table name for storing key-value pairs.
|
|
68
|
+
|
|
69
|
+
## Single values
|
|
70
|
+
|
|
71
|
+
When using single values, each key can be stored only once. When setting an exising key, the original value is
|
|
72
|
+
overridden.
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from sqlalchemy import create_engine
|
|
76
|
+
from sa_values import setup_sa_values, SaValues
|
|
77
|
+
|
|
78
|
+
# connect to db and setup sa-values
|
|
79
|
+
conn = create_engine("sqlite:///:memory:").connect()
|
|
80
|
+
setup_sa_values(conn)
|
|
81
|
+
values = SaValues(conn)
|
|
82
|
+
|
|
83
|
+
if not values.has("some_value"):
|
|
84
|
+
print("Some_value is not stored")
|
|
85
|
+
values.set("some_value", "123")
|
|
86
|
+
if values.has("some_value"):
|
|
87
|
+
print("Some_value is stored")
|
|
88
|
+
print("The value is: ", values.get("some_value"))
|
|
89
|
+
values.set("some_value", "456")
|
|
90
|
+
print("New value is: ", values.get("some_value"))
|
|
91
|
+
|
|
92
|
+
values.delete("some_value")
|
|
93
|
+
if not values.has("some_value"):
|
|
94
|
+
print("Some_value is not stored anymore")
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Multiple values
|
|
99
|
+
|
|
100
|
+
If you want to manage key with more than one value, you can use the `SaValues::multi_value_key(key)`. The method returns
|
|
101
|
+
an object for the multi-value key management. Each value must be unique. In case of duplicate values, the second and
|
|
102
|
+
following values are ignored.
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from sqlalchemy import create_engine
|
|
106
|
+
from sa_values import setup_sa_values, SaValues
|
|
107
|
+
|
|
108
|
+
# connect to db and setup sa-values
|
|
109
|
+
conn = create_engine("sqlite:///:memory:").connect()
|
|
110
|
+
setup_sa_values(conn)
|
|
111
|
+
values = SaValues(conn)
|
|
112
|
+
|
|
113
|
+
multi_key = values.multi_value_key("multi_key")
|
|
114
|
+
|
|
115
|
+
multi_key.add("value1")
|
|
116
|
+
multi_key.add("value2")
|
|
117
|
+
multi_key.add("value2") # do nothing - each value can be stored only once
|
|
118
|
+
multi_key.add("value3")
|
|
119
|
+
|
|
120
|
+
print("Values: ", multi_key.get_all()) # ["value1", "value2", "value3"]
|
|
121
|
+
|
|
122
|
+
multi_key.delete("value2")
|
|
123
|
+
|
|
124
|
+
print("Values: ", multi_key.get()) # ["value1", "value3"]
|
|
125
|
+
|
|
126
|
+
print("value1 exists: ", multi_key.has("value1")) # true
|
|
127
|
+
print("value10 exists: ", multi_key.has("value10")) # false
|
|
128
|
+
|
|
129
|
+
multi_key.clear() # clear all
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Teardown
|
|
133
|
+
|
|
134
|
+
When the `sa-values` is not needed anymore, it can be tear downed by calling the `teardown_sa_values`
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
from sqlalchemy import create_engine
|
|
138
|
+
from sa_values import SaValues, teardown_sa_values
|
|
139
|
+
|
|
140
|
+
# connect to db and setup sa-values
|
|
141
|
+
conn = create_engine("sqlite:///:memory:").connect()
|
|
142
|
+
teardown_sa_values(conn)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
# Buy me a ~~coffee~~ beer
|
|
146
|
+
|
|
147
|
+
If you like this library, or you want to support its development, support me by one
|
|
148
|
+
cold [beer](https://www.buymeacoffee.com/elmordo). The beer is tasty and full of vitamins :-)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/sa_values/__init__.py
|
|
4
|
+
src/sa_values/setup.py
|
|
5
|
+
src/sa_values/table.py
|
|
6
|
+
src/sa_values/values.py
|
|
7
|
+
src/sa_values.egg-info/PKG-INFO
|
|
8
|
+
src/sa_values.egg-info/SOURCES.txt
|
|
9
|
+
src/sa_values.egg-info/dependency_links.txt
|
|
10
|
+
src/sa_values.egg-info/requires.txt
|
|
11
|
+
src/sa_values.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
sa_values
|