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.
@@ -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
+ [![Tests](https://github.com/elmordo/sa-values/actions/workflows/tests.yml/badge.svg)](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
+ [![Tests](https://github.com/elmordo/sa-values/actions/workflows/tests.yml/badge.svg)](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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+ [![Tests](https://github.com/elmordo/sa-values/actions/workflows/tests.yml/badge.svg)](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,9 @@
1
+ sqlalchemy
2
+
3
+ [dev]
4
+ sa-values[test]
5
+ build
6
+ twine
7
+
8
+ [test]
9
+ pytest
@@ -0,0 +1 @@
1
+ sa_values