sweetrpg-db 0.0.1__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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 SweetRPG
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.
@@ -0,0 +1,19 @@
1
+ Metadata-Version: 2.1
2
+ Name: sweetrpg-db
3
+ Version: 0.0.1
4
+ Home-page: https://sweetrpg.com
5
+ Author: Paul Schifferer
6
+ Author-email: dm@sweetrpg.com
7
+ License: MIT
8
+ Project-URL: Documentation, https://sweetrpg.github.io/db
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Environment :: Web Environment
11
+ Classifier: Framework :: Flask
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python
16
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
17
+ License-File: LICENSE
18
+
19
+ # db.py
@@ -0,0 +1 @@
1
+ # db.py
@@ -0,0 +1,60 @@
1
+ [bdist_wheel]
2
+ universal = 1
3
+
4
+ [metadata]
5
+ name = sweetrpg-db
6
+ version = attr: sweetrpg_db.__version__
7
+ url = https://sweetrpg.com
8
+ project_urls =
9
+ Documentation = https://sweetrpg.github.io/db
10
+ license = MIT
11
+ author = Paul Schifferer
12
+ author_email = dm@sweetrpg.com
13
+ long_description = file: README.md
14
+ classifiers =
15
+ Development Status :: 4 - Beta
16
+ Environment :: Web Environment
17
+ Framework :: Flask
18
+ Intended Audience :: Developers
19
+ License :: OSI Approved :: MIT License
20
+ Operating System :: OS Independent
21
+ Programming Language :: Python
22
+ Topic :: Software Development :: Libraries :: Application Frameworks
23
+
24
+ [options]
25
+ packages = find:
26
+ package_dir = = src
27
+ include_package_data = true
28
+ python_required = >=3.12
29
+
30
+ [options.packages.find]
31
+ where = src
32
+
33
+ [options.entry_points]
34
+ console_scripts =
35
+ sweetrpg-db = sweetrpg_db.cli:main
36
+
37
+ [tool:pytest]
38
+ testpaths = tests
39
+ filterwarnings =
40
+ error
41
+
42
+ [coverage:run]
43
+ branch = True
44
+ source =
45
+ sweetrpg_db
46
+ tests
47
+
48
+ [coverage:paths]
49
+ source =
50
+ src
51
+ */site-packages
52
+
53
+ [semantic_release]
54
+ version_variable = src/sweetrpg_db/__init__.py:__version__
55
+ branch = develop
56
+
57
+ [egg_info]
58
+ tag_build =
59
+ tag_date = 0
60
+
@@ -0,0 +1,15 @@
1
+ from setuptools import setup
2
+
3
+ # Metadata goes in setup.cfg. These are here for GitHub's dependency graph.
4
+ setup(
5
+ name="sweetrpg-db",
6
+ install_requires=[
7
+ "dnspython~=2.0",
8
+ "marshmallow~=3.0",
9
+ "mongoengine~=0.27",
10
+ "PyMongo[srv]~=3.0",
11
+ "sweetrpg-model-core",
12
+ "sweetrpg-common",
13
+ ],
14
+ extras_require={},
15
+ )
@@ -0,0 +1,15 @@
1
+ # -*- coding: utf-8 -*-
2
+ __author__ = "Paul Schifferer <dm@sweetrpg.com>"
3
+ """
4
+ Metadata.
5
+ """
6
+
7
+ __title__ = "sweetrpg-db"
8
+ __description__ = "SweetRPG database modules"
9
+ __url__ = "https://github.com/sweetrpg/db"
10
+ __version__ = "0.0.1"
11
+ __build__ = 0x000007
12
+ __author_email__ = "dm@sweetrpg.com"
13
+ __license__ = "MIT"
14
+ __copyright__ = "Copyright 2024 SweetRPG"
15
+ __cake__ = "\u2728 \U0001f370 \u2728"
@@ -0,0 +1,11 @@
1
+ # -*- coding: utf-8 -*-
2
+ __author__ = "Paul Schifferer <dm@sweetrpg.com>"
3
+ """
4
+ Common exceptions.
5
+ """
6
+
7
+
8
+ class ObjectNotFound(Exception):
9
+ """An exception for objects not found."""
10
+
11
+ pass
@@ -0,0 +1,5 @@
1
+ # -*- coding: utf-8 -*-
2
+ __author__ = "Paul Schifferer <dm@sweetrpg.com>"
3
+ """
4
+ Modules related to accessing MongoDB.
5
+ """
@@ -0,0 +1,104 @@
1
+ # -*- coding: utf-8 -*-
2
+ __author__ = "Paul Schifferer <dm@sweetrpg.com>"
3
+ """
4
+ Query options for accessing MongoDB.
5
+ """
6
+
7
+
8
+ class QueryOptions(object):
9
+ """An object to store query options for a PyMongo find*() call."""
10
+
11
+ # https://flask-rest-jsonapi.readthedocs.io/en/latest/filtering.html
12
+ _filter_operators = {
13
+ "eq": "$eq",
14
+ "gt": "$gt",
15
+ "ge": "$gte",
16
+ "in_": "$in",
17
+ "lt": "$lt",
18
+ "le": "$lte",
19
+ "ne": "$ne",
20
+ "notin_": "$nin",
21
+ # '': '$and',
22
+ "isnot": "$not",
23
+ # '': '$nor',
24
+ # '': '$or',
25
+ "is_": "$exists",
26
+ # 'any': 'TODO',
27
+ # 'between': 'TODO',
28
+ # 'endswith': 'TODO',
29
+ # 'has': 'TODO',
30
+ # 'ilike': 'TODO',
31
+ # 'like': 'TODO',
32
+ # 'match': 'TODO',
33
+ # 'notilike': 'TODO',
34
+ # 'notlike': 'TODO',
35
+ # 'startswith': 'TODO',
36
+ }
37
+ _sort_values = {
38
+ "asc": 1,
39
+ "dsc": -1,
40
+ }
41
+
42
+ def __init__(self, filters: dict = {}, projection: list = [], skip: int = 0, limit: int = 0, sort: list = []):
43
+ """Initialize the QueryOptions object.
44
+ :param dict filters: A dictionary of filters to apply to the query.
45
+ :param list projection: A list of attribute names to include in the returned result. If `None`, all attributes are returned.
46
+ :param int skip: An offset to use for pagination.
47
+ :param int limit: The maximum number of results to return.
48
+ :param list sort: A list of key-value pairs specifying the attributes to sort on.
49
+ """
50
+ self.filters = filters
51
+ self.projection = projection
52
+ self.skip = skip
53
+ self.limit = limit
54
+ self.sort = sort
55
+
56
+ def __repr__(self):
57
+ return f"<QueryOptions(filters={self.filters}, projection={self.projection}, skip={self.skip}, limit={self.limit}, sort={self.sort})>"
58
+
59
+ def _process_filter(self, filter_info: dict):
60
+ name = filter_info["name"]
61
+ value = filter_info["val"]
62
+ op = self._filter_operators.get(filter_info["op"], "$eq")
63
+ return {name: {op: value}}
64
+
65
+ def set_filters(self, filters: dict = None, from_querystring: list = None):
66
+ """Sets filters for the query.
67
+
68
+ :param dict filters: A dictionary of filters to set.
69
+ :param list from_querystring: Filters to set in querystring format.
70
+ """
71
+ if filters is not None:
72
+ self.filters = filters
73
+ elif from_querystring is not None:
74
+ filters = {}
75
+ for f in from_querystring:
76
+ filters.update(self._process_filter(f))
77
+ self.filters = filters
78
+
79
+ def set_projection(self, projection: list = None, from_querystring: list = None):
80
+ """Sets projections for the query.
81
+
82
+ :param list projection: A list of field names to include in the query results.
83
+ :param list from_querystring: Projections to set in querystring format.
84
+ """
85
+ if projection is not None:
86
+ self.projection = projection
87
+ elif from_querystring is not None:
88
+ self.projection = from_querystring
89
+
90
+ def _process_sort(self, sort_item: dict):
91
+ name = sort_item["field"]
92
+ direction = self._sort_values.get(sort_item["order"], 1)
93
+ return name, direction
94
+
95
+ def set_sort(self, sort: list = None, from_querystring: list = None):
96
+ """Sets sorting for the query results.
97
+
98
+ :param list sort: A list of dictionaries containing field name and ordering.
99
+ :param list from_querystring: Sorts to set in querystring format.
100
+ """
101
+ if sort is not None:
102
+ self.sort = sort
103
+ elif from_querystring is not None:
104
+ self.sort = list(map(self._process_sort, from_querystring))
@@ -0,0 +1,238 @@
1
+ # -*- coding: utf-8 -*-
2
+ __author__ = "Paul Schifferer <dm@sweetrpg.com>"
3
+ """
4
+ MongoDB repository module.
5
+ """
6
+
7
+ from ..exceptions import ObjectNotFound
8
+ from bson.objectid import ObjectId
9
+ from bson.timestamp import Timestamp
10
+ import datetime
11
+ from .options import QueryOptions
12
+ from pymongo.write_concern import WriteConcern
13
+ import logging
14
+ from mongoengine.queryset import QuerySet
15
+ from mongoengine import Document
16
+
17
+
18
+ class MongoDataRepository(object):
19
+ """A repository class for interacting with a MongoDB database."""
20
+
21
+ def __init__(self, **kwargs):
22
+ """Create a MongoDB repository instance.
23
+
24
+ :param kwargs: Keyword arguments for setting up the repository connection.
25
+ :key model: The class of the model for this connection.
26
+ :key document: The class of the document for this connection.
27
+ :key db: A :class:`PyMongo` object used for connecting to the database.
28
+ """
29
+ self.model_class = kwargs["model"]
30
+ self.document_class = kwargs["document"]
31
+ # self.db = kwargs.get("db")
32
+ # print(dir(self.document_class))
33
+ self.collection = kwargs["collection"] # self.document_class.meta["collection"]
34
+
35
+ def __repr__(self):
36
+ return f"<MongoDataRepository(model_class={self.model_class}, document_class={self.document_class}, collection={self.collection})>"
37
+
38
+ def _handle_value(self, value):
39
+ """Convert a value to a string.
40
+
41
+ :param any value: The value to convert. Supports :class:`bson.objectid.ObjectId`,
42
+ :class:`datetime.datetime`, :class:`bson.timestamp.Timestamp`, and lists of
43
+ any of those types.
44
+ :return str: A string of the specified value.
45
+ """
46
+ if isinstance(value, ObjectId):
47
+ logging.debug("converting ObjectId('%s')...", value)
48
+ return str(value)
49
+ elif isinstance(value, datetime.datetime):
50
+ logging.debug("converting datetime value '%s'...", value)
51
+ d = value.replace(tzinfo=datetime.timezone.utc)
52
+ return d.isoformat(timespec="milliseconds")
53
+ elif isinstance(value, Timestamp):
54
+ logging.debug("converting Timestamp '%s'...", value)
55
+ return value.as_datetime()
56
+ elif isinstance(value, list):
57
+ logging.debug("converting list '%s'...", value)
58
+ return list(map(self._handle_value, value))
59
+
60
+ logging.debug("returning unprocessed value '%s'...", value)
61
+ return value
62
+
63
+ def _modify_record(self, record: dict) -> dict:
64
+ """Modify a record by converting any values to strings, and renaming the internal '_id'
65
+ field to 'id'.
66
+
67
+ :param dict record: The record to modify.
68
+ :return dict: The modified record.
69
+ """
70
+ modified_record = {}
71
+ for k, v in record.items():
72
+ logging.debug("k: %s, v: %s", k, v)
73
+ if k == "_id":
74
+ k = "id"
75
+ modified_value = self._handle_value(v)
76
+ modified_record[k] = modified_value
77
+ logging.debug("k: %s, v (modified): %s", k, modified_value)
78
+
79
+ return modified_record
80
+
81
+ def _adjust_sort(self, sort: tuple) -> str:
82
+ if sort[1] < 0:
83
+ return f"-{sort[0]}"
84
+ return f"+{sort[0]}"
85
+
86
+ def create(self, data: dict) -> Document:
87
+ """Inserts a new object in the database with the data provided.
88
+
89
+ :param dict data: The data for the object
90
+ :return Document: The inserted document.
91
+ """
92
+ logging.debug("data: %s", data)
93
+
94
+ # collection = self.db[self.collection]
95
+ logging.info("Creating new %s record with data %s...", self.document_class.__name__, data)
96
+ doc = self.document_class(**data)
97
+ logging.debug("doc: %s", doc)
98
+ # result = collection.with_options(write_concern=WriteConcern(w=3, j=True)).insert_one(data)
99
+ doc.validate()
100
+ doc.save()
101
+ logging.debug("saved doc: %s", doc)
102
+
103
+ return doc
104
+
105
+ def get(self, record_id: str, deleted: bool = False) -> Document:
106
+ """Fetch a single record from the database.
107
+
108
+ :param str record_id: The identifier for the record to fetch. This value is compared against the attribute specified in `id_attr`.
109
+ :param bool deleted: Include "deleted" objects in the query
110
+ :return Document: An instance of the object type from `model_class`.
111
+ """
112
+ logging.debug("record_id: %s", record_id)
113
+ id_value = record_id
114
+ if isinstance(id_value, str):
115
+ id_value = ObjectId(record_id)
116
+ logging.debug("id_value: %s", id_value)
117
+ query_filter = {"_id": id_value}
118
+ if not deleted:
119
+ query_filter.update({"deleted_at": {"$not": {"$type": "date"}}})
120
+ logging.debug("query_filter: %s", query_filter)
121
+
122
+ logging.info("Fetching %s record for ID %s...", self.document_class.__name__, id_value)
123
+ record = self.document_class.objects(__raw__=query_filter).first() # QuerySet(self.document_class, self.collection)
124
+ # print(f"qs: {qs}")
125
+ # logging.debug("qs: %s", qs)
126
+ # record = None # qs.get(**query_filter)
127
+ logging.debug("record: %s", record)
128
+ # if not record:
129
+ # raise ObjectNotFound(f"Record not found where for '{record_id}'")
130
+
131
+ return record
132
+
133
+ def query(self, options: QueryOptions, deleted: bool = False) -> list:
134
+ """Perform a query for objects in the database.
135
+
136
+ :param QueryOptions options: (Optional) Options specifying limits to the query's returned results
137
+ :return list: Returns a list of Document-subclass instances matching the query.
138
+ """
139
+ logging.debug("options: %s", options)
140
+ query_filter = options.filters or {}
141
+ if not deleted:
142
+ query_filter.update({"deleted_at": {"$not": {"$type": "date"}}})
143
+ logging.debug("query_filter: %s", query_filter)
144
+
145
+ logging.info("Searching for %s records matching filter %s...", self.document_class, query_filter)
146
+ records = (
147
+ self.document_class.objects(__raw__=query_filter)
148
+ .order_by(*list(map(self._adjust_sort, options.sort)))
149
+ .skip(options.skip)
150
+ .limit(options.limit)
151
+ .only(*options.projection)
152
+ )
153
+ # qs = QuerySet(self.document_class, self.collection)
154
+ # if options.skip > 0:
155
+ # qs.skip(options.skip)
156
+ # if options.limit > 0:
157
+ # qs.limit(options.limit)
158
+ # if len(options.projection) > 0:
159
+ # qs.only(*options.projection)
160
+ # if len(options.sort) > 0:
161
+ # qs.order_by(*options.sort)
162
+ # print(f"qs: {qs}")
163
+ # logging.debug("qs: %s", qs)
164
+ # query_set = self.document_class.objects.skip(options.skip).limit(options.limit).order_by(options.sort).only(*options.projection)
165
+ # records = list(qs.all()) # .all()
166
+ logging.debug("records: %s", records)
167
+
168
+ # modified_records = map(self._modify_record, records)
169
+ # logging.debug("modified_records: %s", modified_records)
170
+
171
+ return list(records)
172
+
173
+ def update(self, record_id: str, update: dict, deleted: bool = False) -> Document:
174
+ """Update the specified record.
175
+
176
+ :param str record_id: The ID of the record to update.
177
+ :param dict update: The data to update for the record.
178
+ :param bool deleted: Indicates whether the update operation should look for deleted records.
179
+ :return Document: The update version of the object.
180
+ """
181
+ id_value = record_id
182
+ if isinstance(id_value, str):
183
+ id_value = ObjectId(record_id)
184
+ # if self.id_attr == "_id":
185
+ # logging.debug("ID attribute is '_id', converting to ObjectId")
186
+ # id_value = ObjectId(record_id)
187
+ obj_filter = {"_id": id_value}
188
+ logging.debug("obj_filter: %s", obj_filter)
189
+
190
+ update_oper = {"$set": update}
191
+ logging.debug("update_oper: %s", update_oper)
192
+
193
+ logging.info("Marking %s record %s deleted...", self.model_class, id_value)
194
+ query_filter = {"_id": id_value}
195
+ if not deleted:
196
+ query_filter.update({"deleted_at": {"$not": {"$type": "date"}}})
197
+ logging.debug("query_filter: %s", query_filter)
198
+ # doc = self.document_class.objects.raw(query_filter).update(update_oper)
199
+ doc = self.get(record_id, deleted=deleted)
200
+ # if doc is None:
201
+ # logging.info("No document found to update for record ID %s.", record_id)
202
+ # return False
203
+ logging.debug("doc: %s", doc)
204
+ doc.update(**update)
205
+
206
+ return doc
207
+
208
+ def delete(self, record_id: str, actually: bool = False) -> bool:
209
+ """'Delete' the specified record. Deletion is accomplished by setting the `deleted_at` field to the current
210
+ timestamp, so that queries for the object will ignore it.
211
+
212
+ :param str record_id: The record ID of the object to delete. This can be a string or :class:`bson.objectid.ObjectId`.
213
+ :param bool actually: Actually delete the record instead of just marking it "deleted".
214
+ :return bool: A boolean indicating whether the record was able to be marked deleted.
215
+ :raises DoesNotExist:
216
+ """
217
+ id_value = record_id
218
+ if isinstance(id_value, str):
219
+ id_value = ObjectId(record_id)
220
+ doc = self.get(record_id)
221
+ # if doc is None:
222
+ # logging.info("No document found to delete for record ID %s.", record_id)
223
+ # return False
224
+
225
+ if actually:
226
+ logging.info("Deleting %s record %s...", self.model_class.__name__, id_value)
227
+ doc.delete()
228
+ return True
229
+ else:
230
+ logging.info("Marking %s record %s deleted...", self.model_class.__name__, id_value)
231
+ now = datetime.datetime.utcnow()
232
+ doc.deleted_at = now
233
+ updated_doc = doc.save()
234
+ logging.debug("updated_doc: %s", updated_doc)
235
+
236
+ return True
237
+
238
+ return False
@@ -0,0 +1,5 @@
1
+ # -*- coding: utf-8 -*-
2
+ __author__ = "Paul Schifferer <dm@sweetrpg.com>"
3
+ """
4
+ Schema modules.
5
+ """
@@ -0,0 +1,5 @@
1
+ # -*- coding: utf-8 -*-
2
+ __author__ = "Paul Schifferer <dm@sweetrpg.com>"
3
+ """
4
+ ... nothing
5
+ """
@@ -0,0 +1,19 @@
1
+ Metadata-Version: 2.1
2
+ Name: sweetrpg-db
3
+ Version: 0.0.1
4
+ Home-page: https://sweetrpg.com
5
+ Author: Paul Schifferer
6
+ Author-email: dm@sweetrpg.com
7
+ License: MIT
8
+ Project-URL: Documentation, https://sweetrpg.github.io/db
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Environment :: Web Environment
11
+ Classifier: Framework :: Flask
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python
16
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
17
+ License-File: LICENSE
18
+
19
+ # db.py
@@ -0,0 +1,17 @@
1
+ LICENSE
2
+ README.md
3
+ setup.cfg
4
+ setup.py
5
+ src/sweetrpg_db/__init__.py
6
+ src/sweetrpg_db/exceptions.py
7
+ src/sweetrpg_db/utils.py
8
+ src/sweetrpg_db.egg-info/PKG-INFO
9
+ src/sweetrpg_db.egg-info/SOURCES.txt
10
+ src/sweetrpg_db.egg-info/dependency_links.txt
11
+ src/sweetrpg_db.egg-info/entry_points.txt
12
+ src/sweetrpg_db.egg-info/requires.txt
13
+ src/sweetrpg_db.egg-info/top_level.txt
14
+ src/sweetrpg_db/mongodb/__init__.py
15
+ src/sweetrpg_db/mongodb/options.py
16
+ src/sweetrpg_db/mongodb/repo.py
17
+ src/sweetrpg_db/schema/__init__.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ sweetrpg-db = sweetrpg_db.cli:main
@@ -0,0 +1,6 @@
1
+ dnspython~=2.0
2
+ marshmallow~=3.0
3
+ mongoengine~=0.27
4
+ PyMongo[srv]~=3.0
5
+ sweetrpg-model-core
6
+ sweetrpg-common
@@ -0,0 +1 @@
1
+ sweetrpg_db