├── .github
└── workflows
│ └── release.yml
├── .gitignore
├── LICENSE
├── README.md
├── pantherdb
├── __init__.py
└── pantherdb.py
├── ruff.toml
├── setup.py
└── tests
├── __init__.py
└── test_normal.py
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | push:
5 | tags:
6 | - 'v*'
7 |
8 | jobs:
9 | test:
10 | name: Test
11 | runs-on: ubuntu-latest
12 | strategy:
13 | matrix:
14 | python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
15 |
16 | steps:
17 | - name: Checkout source
18 | uses: actions/checkout@v3
19 |
20 | - name: Set up Python
21 | uses: actions/setup-python@v4
22 | with:
23 | python-version: ${{ matrix.python }}
24 |
25 | - name: Install PantherDB
26 | run: python -m pip install .
27 |
28 | - name: Install Faker
29 | run: python -m pip install Faker
30 |
31 | - name: Run Tests
32 | run: python -m unittest tests/test_*.py
33 |
34 | build-n-publish:
35 | name: Build and publish to PyPI
36 | runs-on: ubuntu-latest
37 | needs: [test]
38 | steps:
39 | - name: Checkout source
40 | uses: actions/checkout@v3
41 |
42 | - name: Set up Python
43 | uses: actions/setup-python@v4
44 | with:
45 | python-version: '3.10'
46 |
47 | - name: Build source and wheel distributions
48 | run: |
49 | python -m pip install --upgrade build twine
50 | python -m build
51 | twine check --strict dist/*
52 |
53 | - name: Publish distribution to PyPI
54 | uses: pypa/gh-action-pypi-publish@master
55 | with:
56 | user: __token__
57 | password: ${{ secrets.PYPI_TOKEN }}
58 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .venv*
2 | .idea/
3 | build/
4 | __pycache__/
5 | *.pdb
6 | dist/
7 | pantherdb.egg-info/
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Ali RajabNezhad
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | [](https://pypi.org/project/pantherdb/) [](https://pypi.org/project/pantherdb/) [](https://pepy.tech/project/pantherdb) [](https://github.com/alirn76/pantherdb/blob/main/LICENSE)
3 |
4 | ## Introduction
5 |
6 | PantherDB is a Simple, FileBase and Document Oriented database that you can use in your projects.
7 |
8 | ### Features:
9 | - Document Oriented
10 | - Easy to use
11 | - Written in pure Python +3.8 based on standard type hints
12 | - Handle Database Encryption
13 | - Singleton connection per `db_name`
14 |
15 | ## Usage
16 |
17 | ### Database:
18 | - #### Create a database:
19 | ```python
20 | db: PantherDB = PantherDB('database.pdb')
21 | ```
22 |
23 | - #### Create an encrypted database:
24 | Required `cyptography` install it with `pip install pantherdb[full]`
25 | ```python
26 | from cryptography.fernet import Fernet
27 | key = Fernet.generate_key() # Should be static (You should not generate new key on every run)
28 | db: PantherDB = PantherDB('database.pdb', secret_key=key)
29 | ```
30 |
31 | - #### Access to a collection:
32 | ```python
33 | user_collection: PantherCollection = db.collection('User')
34 | ```
35 |
36 | - #### Delete a collection:
37 | ```python
38 | db.collection('User').drop()
39 | ```
40 | ### Create:
41 | - #### Insert document:
42 | ```python
43 | user: PantherDocument = db.collection('User').insert_one(first_name='Ali', last_name='Rn')
44 | ```
45 |
46 | ### Get:
47 | - #### Find one document:
48 | ```python
49 | user: PantherDocument = db.collection('User').find_one(first_name='Ali', last_name='Rn')
50 | ```
51 | or
52 | ```python
53 | user: PantherDocument = db.collection('User').find_one()
54 | ```
55 |
56 | - #### Find first document (alias of `find_one()`):
57 | ```python
58 | user: PantherDocument = db.collection('User').first(first_name='Ali', last_name='Rn')
59 | ```
60 | or
61 | ```python
62 | user: PantherDocument = db.collection('User').first()
63 | ```
64 |
65 | - #### Find last document:
66 | ```python
67 | user: PantherDocument = db.collection('User').last(first_name='Ali', last_name='Rn')
68 | ```
69 | or
70 | ```python
71 | user: PantherDocument = db.collection('User').last()
72 | ```
73 |
74 | - #### Find documents:
75 | ```python
76 | users: list[PantherDocument] = db.collection('User').find(last_name='Rn')
77 | ```
78 | or all documents
79 | ```python
80 | users: list[PantherDocument] = db.collection('User').find()
81 | ```
82 |
83 | - #### Count documents:
84 | ```python
85 | users_count: int = db.collection('User').count(first_name='Ali')
86 | ```
87 |
88 | ### Update:
89 | - #### Update a document:
90 | ```python
91 | user: PantherDocument = db.collection('User').find_one(first_name='Ali', last_name='Rn')
92 | user.update(name='Saba')
93 | ```
94 |
95 | - #### Filter and Update a document:
96 | ```python
97 | _filter = {'first_name': 'Ali', 'last_name': 'Rn'}
98 | is_updated: bool = db.collection('User').update_one(_filter, first_name='Saba')
99 | ```
100 |
101 | - #### Filter and Update many:
102 | ```python
103 | _filter = {'first_name': 'Ali'}
104 | updated_count: int = db.collection('User').update_many(_filter, first_name='Saba')
105 | ```
106 |
107 | ### Delete:
108 | - #### Delete a document:
109 | ```python
110 | user: PantherDocument = db.collection('User').first(first_name='Ali', last_name='Rn')
111 | user.delete()
112 | ```
113 |
114 | - #### Filter and Delete a document:
115 | ```python
116 | is_deleted: bool = db.collection('User').delete_one(first_name='Ali', last_name='Rn')
117 | ```
118 |
119 | - #### Filter and Delete many:
120 | ```python
121 | deleted_count: int = db.collection('User').delete_many(last_name='Rn')
122 | ```
123 |
124 | ## TODO:
125 | - [x] Add encryption
126 | - [ ] Complete tests TODO
127 | - [ ] Add B+ tree
128 |
--------------------------------------------------------------------------------
/pantherdb/__init__.py:
--------------------------------------------------------------------------------
1 | from pantherdb.pantherdb import PantherDB, PantherCollection, PantherDocument, PantherDBException, Cursor
2 |
3 | __version__ = '2.2.3'
4 |
--------------------------------------------------------------------------------
/pantherdb/pantherdb.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import asyncio
4 | from pathlib import Path
5 | from threading import Thread
6 | from typing import ClassVar, Iterator, Any, List, Tuple, Union
7 |
8 | import orjson as json
9 | import ulid
10 |
11 |
12 | class PantherDBException(Exception):
13 | pass
14 |
15 |
16 | class PantherDB:
17 | _instances: ClassVar[dict] = {}
18 | db_name: str = 'database.pdb'
19 | __secret_key: bytes | None
20 | __fernet: Any # type: cryptography.fernet.Fernet | None
21 | __return_dict: bool
22 | __return_cursor: bool
23 | __content: dict
24 |
25 | def __new__(cls, *args, **kwargs):
26 | if cls.__name__ != 'PantherDB':
27 | return super().__new__(cls)
28 |
29 | if args:
30 | db_name = args[0] or cls.db_name
31 | elif 'db_name' in kwargs:
32 | db_name = kwargs['db_name'] or cls.db_name
33 | else:
34 | db_name = cls.db_name
35 |
36 | db_name = str(db_name) # Can be PosixPath
37 | # Replace with .removesuffix('.pdb') after python3.8 compatible support
38 | if db_name.endswith('.pdb'):
39 | db_name = db_name[:-4]
40 | elif db_name.endswith('.json'):
41 | db_name = db_name[:-5]
42 |
43 | if db_name not in cls._instances:
44 | cls._instances[db_name] = super().__new__(cls)
45 | return cls._instances[db_name]
46 |
47 | def __init__(
48 | self,
49 | db_name: str | None = None,
50 | *,
51 | return_dict: bool = False,
52 | return_cursor: bool = False,
53 | secret_key: bytes | None = None,
54 | ):
55 | self.__return_dict = return_dict
56 | self.__return_cursor = return_cursor
57 | self.__secret_key = secret_key
58 | self.__content = {}
59 | if self.__secret_key:
60 | from cryptography.fernet import Fernet
61 |
62 | self.__fernet = Fernet(self.__secret_key)
63 | else:
64 | self.__fernet = None
65 |
66 | db_name = str(db_name) # Can be PosixPath
67 | if db_name:
68 | if not db_name.endswith(('pdb', 'json')):
69 | if self.__secret_key:
70 | db_name = f'{db_name}.pdb'
71 | else:
72 | db_name = f'{db_name}.json'
73 | self.db_name = db_name
74 | Path(self.db_name).touch(exist_ok=True)
75 |
76 | def __str__(self) -> str:
77 | self._refresh()
78 | db = ',\n'.join(f' {k}: {len(v)} documents' for k, v in self.content.items())
79 | return f'{self.__class__.__name__}(\n{db}\n)'
80 |
81 | __repr__ = __str__
82 |
83 | @property
84 | def content(self) -> dict:
85 | return self.__content
86 |
87 | @property
88 | def return_cursor(self) -> bool:
89 | return self.__return_cursor
90 |
91 | @property
92 | def return_dict(self) -> bool:
93 | return self.__return_dict
94 |
95 | @property
96 | def secret_key(self) -> bytes | None:
97 | return self.__secret_key
98 |
99 | def _write(self) -> None:
100 | content = json.dumps(self.content)
101 |
102 | if self.secret_key:
103 | content = self.__fernet.encrypt(content)
104 |
105 | with open(self.db_name, 'bw') as file:
106 | file.write(content)
107 |
108 | def _refresh(self) -> None:
109 | with open(self.db_name, 'rb') as file:
110 | data = file.read()
111 |
112 | if not data:
113 | self.__content = {}
114 |
115 | elif not self.secret_key:
116 | self.__content = json.loads(data)
117 |
118 | else:
119 | try:
120 | decrypted_data: bytes = self.__fernet.decrypt(data)
121 | except Exception: # type[cryptography.fernet.InvalidToken]
122 | error = '"secret_key" Is Not Valid'
123 | raise PantherDBException(error)
124 |
125 | self.__content = json.loads(decrypted_data)
126 |
127 | def collection(self, collection_name: str) -> PantherCollection:
128 | return PantherCollection(
129 | db_name=self.db_name,
130 | collection_name=collection_name,
131 | return_dict=self.return_dict,
132 | return_cursor=self.return_cursor,
133 | secret_key=self.secret_key,
134 | )
135 |
136 | def close(self):
137 | pass
138 |
139 |
140 | class PantherCollection(PantherDB):
141 | __collection_name: str
142 |
143 | def __init__(
144 | self,
145 | db_name: str,
146 | *,
147 | collection_name: str,
148 | return_dict: bool,
149 | return_cursor: bool,
150 | secret_key: bytes,
151 | ):
152 | super().__init__(db_name=db_name, return_dict=return_dict, return_cursor=return_cursor, secret_key=secret_key)
153 | self.__collection_name = collection_name
154 |
155 | def __str__(self) -> str:
156 | self._refresh()
157 |
158 | if self.collection_name not in self.content or (documents := self.content[self.collection_name]):
159 | result = ''
160 | else:
161 | result = '\n'.join(f' {k}: {type(v).__name__}' for k, v in documents[0].items())
162 |
163 | return f'{self.__class__.__name__}(\n collection_name: {self.collection_name}\n\n{result}\n)'
164 |
165 | @property
166 | def collection_name(self) -> str:
167 | return self.__collection_name
168 |
169 | def __check_is_panther_document(self) -> None:
170 | if not hasattr(self, '_id'):
171 | raise PantherDBException('You should call this method on PantherDocument instance.')
172 |
173 | def __create_result(self, data: dict, /) -> PantherDocument | dict:
174 | if self.return_dict:
175 | return data
176 |
177 | return PantherDocument(
178 | db_name=self.db_name,
179 | collection_name=self.collection_name,
180 | return_dict=self.return_dict,
181 | return_cursor=self.return_cursor,
182 | secret_key=self.secret_key,
183 | **data,
184 | )
185 |
186 | def _write_collection(self, documents: list) -> None:
187 | self.content[self.collection_name] = documents
188 | self._write()
189 |
190 | def _drop_collection(self) -> None:
191 | self._refresh()
192 | if self.collection_name in self.content:
193 | del self.content[self.collection_name]
194 | self._write()
195 |
196 | def _get_collection(self) -> list[dict]:
197 | """Return documents"""
198 | self._refresh()
199 | return self.content.get(self.collection_name, [])
200 |
201 | def _find(self, _documents: list, /, **kwargs) -> Iterator[tuple[int, PantherDocument | dict]]:
202 | found = False
203 | for index, document in enumerate(_documents):
204 | for k, v in kwargs.items():
205 | if document.get(k) != v:
206 | break
207 | else:
208 | found = True
209 | yield index, self.__create_result(document)
210 |
211 | if not found:
212 | yield None, None
213 |
214 | def find_one(self, **kwargs) -> PantherDocument | dict | None:
215 | documents = self._get_collection()
216 |
217 | # Empty Collection
218 | if not documents:
219 | return None
220 |
221 | if not kwargs:
222 | return self.__create_result(documents[0])
223 |
224 | for _, d in self._find(documents, **kwargs):
225 | # Return the first document
226 | return d
227 |
228 | def find(self, **kwargs) -> Cursor | List[PantherDocument | dict]:
229 | documents = self._get_collection()
230 |
231 | result = [d for _, d in self._find(documents, **kwargs) if d is not None]
232 |
233 | if self.return_cursor:
234 | return Cursor(result, kwargs)
235 | return result
236 |
237 | def first(self, **kwargs) -> PantherDocument | dict | None:
238 | return self.find_one(**kwargs)
239 |
240 | def last(self, **kwargs) -> PantherDocument | dict | None:
241 | documents = self._get_collection()
242 | documents.reverse()
243 |
244 | # Empty Collection
245 | if not documents:
246 | return None
247 |
248 | if not kwargs:
249 | return self.__create_result(documents[0])
250 |
251 | for _, d in self._find(documents, **kwargs):
252 | # Return the first one
253 | return d
254 |
255 | def insert_one(self, **kwargs) -> PantherDocument | dict:
256 | documents = self._get_collection()
257 | kwargs['_id'] = ulid.new()
258 | documents.append(kwargs)
259 | self._write_collection(documents)
260 | return self.__create_result(kwargs)
261 |
262 | def delete(self) -> None:
263 | self.__check_is_panther_document()
264 | documents = self._get_collection()
265 | for d in documents:
266 | if d.get('_id') == self._id: # noqa: Unresolved References
267 | documents.remove(d)
268 | self._write_collection(documents)
269 | break
270 |
271 | def delete_one(self, **kwargs) -> bool:
272 | documents = self._get_collection()
273 |
274 | # Empty Collection
275 | if not documents:
276 | return False
277 |
278 | if not kwargs:
279 | return False
280 |
281 | for i, _ in self._find(documents, **kwargs):
282 | if i is None:
283 | # Didn't find any match
284 | return False
285 |
286 | # Delete matched one and return
287 | documents.pop(i)
288 | self._write_collection(documents)
289 | return True
290 |
291 | def delete_many(self, **kwargs) -> int:
292 | documents = self._get_collection()
293 |
294 | # Empty Collection
295 | if not documents:
296 | return 0
297 |
298 | if not kwargs:
299 | return 0
300 |
301 | indexes = [i for i, _ in self._find(documents, **kwargs) if i is not None]
302 |
303 | # Delete Matched Indexes
304 | for i in indexes[::-1]:
305 | documents.pop(i)
306 | self._write_collection(documents)
307 | return len(indexes)
308 |
309 | def update(self, **kwargs) -> None:
310 | self.__check_is_panther_document()
311 | documents = self._get_collection()
312 | kwargs.pop('_id', None)
313 |
314 | for d in documents:
315 | if d.get('_id') == self._id: # noqa: Unresolved References
316 | for k, v in kwargs.items():
317 | d[k] = v
318 | setattr(self, k, v)
319 | self._write_collection(documents)
320 |
321 | def update_one(self, condition: dict, **kwargs) -> bool:
322 | documents = self._get_collection()
323 | result = False
324 |
325 | if not condition:
326 | return result
327 |
328 | kwargs.pop('_id', None)
329 | for d in documents:
330 | for k, v in condition.items():
331 | if d.get(k) != v:
332 | break
333 | else:
334 | result = True
335 | for new_k, new_v in kwargs.items():
336 | d[new_k] = new_v
337 | self._write_collection(documents)
338 | break
339 |
340 | return result
341 |
342 | def update_many(self, condition: dict, **kwargs) -> int:
343 | documents = self._get_collection()
344 | if not condition:
345 | return 0
346 |
347 | kwargs.pop('_id', None)
348 | updated_count = 0
349 | for d in documents:
350 | for k, v in condition.items():
351 | if d.get(k) != v:
352 | break
353 | else:
354 | updated_count += 1
355 | for new_k, new_v in kwargs.items():
356 | d[new_k] = new_v
357 |
358 | if updated_count:
359 | self._write_collection(documents)
360 | return updated_count
361 |
362 | def count(self, **kwargs) -> int:
363 | documents = self._get_collection()
364 | if not kwargs:
365 | return len(documents)
366 |
367 | return len([i for i, _ in self._find(documents, **kwargs) if i is not None])
368 |
369 | def drop(self) -> None:
370 | self._drop_collection()
371 |
372 |
373 | class PantherDocument(PantherCollection):
374 | __data: dict
375 |
376 | def __init__(
377 | self,
378 | db_name: str,
379 | *,
380 | collection_name: str,
381 | return_dict: bool,
382 | return_cursor: bool,
383 | secret_key: bytes,
384 | **kwargs,
385 | ):
386 | self.__data = kwargs
387 | super().__init__(
388 | db_name=db_name,
389 | collection_name=collection_name,
390 | return_dict=return_dict,
391 | return_cursor=return_cursor,
392 | secret_key=secret_key,
393 | )
394 |
395 | def __str__(self) -> str:
396 | items = ', '.join(f'{k}={v}' for k, v in self.data.items())
397 | return f'{self.collection_name}({items})'
398 |
399 | __repr__ = __str__
400 |
401 | def __getattr__(self, item: str):
402 | try:
403 | return object.__getattribute__(self, '_PantherDocument__data')[item]
404 | except KeyError:
405 | error = f'Invalid Collection Field: "{item}"'
406 | raise PantherDBException(error)
407 |
408 | def __setattr__(self, key, value):
409 | if key not in [
410 | '_PantherDB__return_dict',
411 | '_PantherDB__return_cursor',
412 | '_PantherDB__secret_key',
413 | '_PantherDB__content',
414 | '_PantherDB__fernet',
415 | '_PantherDB__ulid',
416 | '_PantherCollection__collection_name',
417 | '_PantherDocument__data',
418 | ]:
419 | try:
420 | object.__getattribute__(self, key)
421 | except AttributeError:
422 | self.data[key] = value
423 | return
424 |
425 | super().__setattr__(key, value)
426 |
427 | __setitem__ = __setattr__
428 |
429 | __getitem__ = __getattr__
430 |
431 | @property
432 | def id(self) -> int:
433 | return self.data['_id']
434 |
435 | @property
436 | def data(self) -> dict:
437 | return self.__data
438 |
439 | def save(self) -> None:
440 | """Pop & Insert New Document"""
441 | documents = self._get_collection()
442 | for i, d in enumerate(documents):
443 | if d['_id'] == self.id:
444 | documents.pop(i)
445 | documents.insert(i, self.data)
446 | break
447 | self._write_collection(documents)
448 |
449 | def json(self) -> str:
450 | return json.dumps(self.data).decode()
451 |
452 |
453 | class Cursor:
454 | def __init__(self, documents: List[dict | PantherDocument], kwargs: dict):
455 | self.documents = documents
456 | self.filter = kwargs # Used in Panther
457 | self._cursor = -1
458 | self._limit = None
459 | self._sorts = None
460 | self._skip = None
461 | self.cls = None
462 | self.response_type = None
463 | self._conditions_applied = False
464 |
465 | def __aiter__(self):
466 | return self
467 |
468 | async def next(self, is_async: bool = False):
469 | error = StopAsyncIteration if is_async else StopIteration
470 |
471 | if not self._conditions_applied:
472 | self._apply_conditions()
473 |
474 | self._cursor += 1
475 | if self._limit and self._cursor > self._limit:
476 | raise error
477 |
478 | try:
479 | result = self.documents[self._cursor]
480 | except IndexError:
481 | raise error
482 |
483 | # Return pure result
484 | if self.response_type is None:
485 | return result
486 |
487 | # Convert the result then return
488 | if self.is_function_async(self.response_type):
489 | return await self.response_type(result)
490 | return self.response_type(result)
491 |
492 | def __next__(self):
493 | return self._run_coroutine(self.next())
494 |
495 | async def __anext__(self):
496 | return await self.next(is_async=True)
497 |
498 | def __getitem__(self, index: int | slice) -> Union[Cursor, dict, ...]:
499 | if not self._conditions_applied:
500 | self._apply_conditions()
501 |
502 | result = self.documents[index]
503 | if isinstance(index, int) and self.response_type:
504 | return self._run_coroutine(self.response_type(result))
505 | return result
506 |
507 | def sort(self, sorts: List[Tuple[str, int]] | str, sort_order: int = None):
508 | if isinstance(sorts, str):
509 | self._sorts = [(sorts, sort_order)]
510 | else:
511 | self._sorts = sorts
512 | return self
513 |
514 | def skip(self, skip):
515 | self._skip = skip
516 | return self
517 |
518 | def limit(self, limit: int):
519 | self._limit = limit
520 | return self
521 |
522 | def _apply_conditions(self):
523 | self._apply_sort()
524 | self._apply_skip()
525 | self._apply_limit()
526 | self._conditions_applied = True
527 |
528 | def _apply_sort(self):
529 | if self._sorts:
530 | for sort in self._sorts[::-1]:
531 | self.documents.sort(key=lambda x: x[sort[0]], reverse=bool(sort[1] == -1))
532 |
533 | def _apply_skip(self):
534 | if self._skip:
535 | self.documents = self.documents[self._skip:]
536 |
537 | def _apply_limit(self):
538 | if self._limit:
539 | self.documents = self.documents[:self._limit]
540 |
541 | @classmethod
542 | def _run_coroutine(cls, coroutine):
543 | try:
544 | # Check if there's an event loop already running in this thread
545 | asyncio.get_running_loop()
546 | except RuntimeError:
547 | # No event loop is running in this thread — safe to use asyncio.run
548 | return asyncio.run(coroutine)
549 |
550 | # Since we cannot block a running event loop with run_until_complete,
551 | # we execute the coroutine in a separate thread with its own event loop.
552 | result = []
553 |
554 | def run_in_thread():
555 | new_loop = asyncio.new_event_loop()
556 | asyncio.set_event_loop(new_loop)
557 | try:
558 | result.append(new_loop.run_until_complete(coroutine))
559 | finally:
560 | new_loop.close()
561 | thread = Thread(target=run_in_thread)
562 | thread.start()
563 | thread.join()
564 | return result[0]
565 |
566 |
567 | @classmethod
568 | def is_function_async(cls, func: Callable) -> bool:
569 | """
570 | Sync result is 0 --> False
571 | async result is 128 --> True
572 | """
573 | return bool(func.__code__.co_flags & (1 << 7))
574 |
--------------------------------------------------------------------------------
/ruff.toml:
--------------------------------------------------------------------------------
1 | exclude = [
2 | ".venv",
3 | "build",
4 | ".venv3.8",
5 | ".venv312",
6 | ".git",
7 | ]
8 | select = [
9 | "ALL"
10 | ]
11 | ignore = [
12 | "C901", "N818", "N805", "D1", "D2", "D400", "D415", "D401", "YTT", "ANN", "S", "B", "C4", "DTZ", "EM", "EXE",
13 | "FA", "INP", "PYI024", "PT", "RET503", "RET505", "ARG", "PTH123", "TD", "FIX", "PL", "RUF013", "TRY003", "TRY200"
14 | ]
15 |
16 | line-length = 120
17 | [flake8-quotes]
18 | inline-quotes = "single"
19 |
20 | [per-file-ignores]
21 | "pantherdb/__init__.py" = ["F405"]
22 | "pantherdb/pantherdb.py" = ["A003"]
23 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 | from setuptools import setup
4 |
5 |
6 | def pantherdb_version() -> str:
7 | with open('pantherdb/__init__.py') as f:
8 | return re.search("__version__ = ['\"]([^'\"]+)['\"]", f.read()).group(1)
9 |
10 |
11 | VERSION = pantherdb_version()
12 | with open('README.md') as file:
13 | DESCRIPTION = file.read()
14 |
15 | EXTRAS_REQUIRE = {
16 | 'full': [
17 | 'cryptography~=41.0',
18 | ],
19 | }
20 |
21 |
22 | setup(
23 | name='pantherdb',
24 | version=VERSION,
25 | python_requires='>=3.8',
26 | author='Ali RajabNezhad',
27 | author_email='alirn76@yahoo.com',
28 | url='https://github.com/PantherPy/pantherdb',
29 | description='is a Simple, FileBase and Document Oriented database',
30 | long_description=DESCRIPTION,
31 | long_description_content_type='text/markdown',
32 | include_package_data=True,
33 | license='MIT',
34 | classifiers=[
35 | 'Operating System :: OS Independent',
36 | 'License :: OSI Approved :: MIT License',
37 | 'Programming Language :: Python :: 3.8',
38 | 'Programming Language :: Python :: 3.9',
39 | 'Programming Language :: Python :: 3.10',
40 | 'Programming Language :: Python :: 3.11',
41 | 'Programming Language :: Python :: 3.12',
42 | 'Programming Language :: Python :: 3.13',
43 | ],
44 | install_requires=[
45 | 'orjson~=3.9.15',
46 | 'simple-ulid~=1.0.0'
47 | ],
48 | extras_require=EXTRAS_REQUIRE,
49 | )
50 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PantherPy/pantherdb/dc80aeeb291b466955ee1c79e478d0fb21a58ca0/tests/__init__.py
--------------------------------------------------------------------------------
/tests/test_normal.py:
--------------------------------------------------------------------------------
1 |
2 | from pathlib import Path
3 | from unittest import TestCase, IsolatedAsyncioTestCase
4 | from uuid import uuid4
5 |
6 | import orjson as json
7 | from faker import Faker
8 |
9 | from pantherdb import PantherCollection, PantherDB, PantherDocument, Cursor
10 |
11 | f = Faker()
12 |
13 |
14 | class TestNormalPantherDB(TestCase):
15 |
16 | @classmethod
17 | def setUp(cls):
18 | cls.db_name = uuid4().hex
19 | cls.db_name = f'{cls.db_name}.pdb'
20 | cls.db = PantherDB(db_name=cls.db_name)
21 |
22 | @classmethod
23 | def tearDown(cls):
24 | Path(cls.db_name).unlink()
25 |
26 | @classmethod
27 | def create_junk_document(cls, collection) -> int:
28 | _count = f.random.randint(2, 10)
29 | for i in range(_count):
30 | collection.insert_one(first_name=f'{f.first_name()}{i}', last_name=f'{f.last_name()}{i}')
31 | return _count
32 |
33 | # Singleton
34 | def test_pantherdb_singleton(self):
35 | test_1 = PantherDB(db_name='test1')
36 | test_2 = PantherDB('test1')
37 | assert test_1 == test_2
38 |
39 | default_1 = PantherDB()
40 | default_2 = PantherDB()
41 | assert default_1 == default_2
42 |
43 | assert test_1 != default_1
44 | assert test_2 != default_2
45 |
46 | Path(test_1.db_name).unlink()
47 | Path(default_1.db_name).unlink()
48 |
49 | # Create DB
50 | def test_creation_of_db(self):
51 | assert Path(self.db_name).exists()
52 | assert Path(self.db_name).is_file()
53 | assert self.db.db_name == self.db_name
54 |
55 | def test_creation_of_db_without_extension(self):
56 | db_name = uuid4().hex
57 | db = PantherDB(db_name=db_name)
58 | final_db_name = f'{db_name}.json'
59 |
60 | assert Path(final_db_name).exists()
61 | assert Path(final_db_name).is_file()
62 | assert db.db_name == final_db_name
63 |
64 | Path(final_db_name).unlink()
65 |
66 | def test_creation_of_collection(self):
67 | collection_name = f.word()
68 | collection = self.db.collection(collection_name)
69 |
70 | assert bool(collection)
71 | assert isinstance(collection, PantherCollection)
72 | assert collection.collection_name == collection_name
73 | assert collection.content == {}
74 | assert collection.secret_key is None
75 |
76 | # Drop
77 | def test_drop_collection(self):
78 | collection = self.db.collection(f.word())
79 | collection.drop()
80 | assert collection.content == {}
81 |
82 | # Insert
83 | def test_insert_one(self):
84 | collection = self.db.collection(f.word())
85 | first_name = f.first_name()
86 | last_name = f.last_name()
87 | obj = collection.insert_one(first_name=first_name, last_name=last_name)
88 |
89 | assert isinstance(obj, PantherDocument)
90 | assert obj.first_name == first_name
91 | assert obj.last_name == last_name
92 |
93 | def test_id_assignments(self):
94 | collection = self.db.collection(f.word())
95 | ids = set()
96 | _count = f.random.randint(2, 10)
97 | for i in range(_count):
98 | obj = collection.insert_one(first_name=f.first_name(), last_name=f.last_name())
99 | ids.add(obj.id)
100 | assert len(obj.id) == 26
101 |
102 | # Each id should be unique
103 | assert len(ids) == _count
104 |
105 | # Find One
106 | def test_find_one_first(self):
107 | collection = self.db.collection(f.word())
108 | first_name = f.first_name()
109 | last_name = f.last_name()
110 |
111 | # Insert with specific names
112 | collection.insert_one(first_name=first_name, last_name=last_name)
113 |
114 | # Add others
115 | self.create_junk_document(collection)
116 |
117 | # Find
118 | obj = collection.find_one(first_name=first_name, last_name=last_name)
119 | assert isinstance(obj, PantherDocument)
120 | assert obj.first_name == first_name
121 | assert obj.last_name == last_name
122 |
123 | def test_find_one_last(self):
124 | collection = self.db.collection(f.word())
125 | first_name = f.first_name()
126 | last_name = f.last_name()
127 |
128 | # Add others
129 | self.create_junk_document(collection)
130 |
131 | # Insert with specific names
132 | collection.insert_one(first_name=first_name, last_name=last_name)
133 |
134 | # Find
135 | obj = collection.find_one(first_name=first_name, last_name=last_name)
136 | assert isinstance(obj, PantherDocument)
137 | assert obj.first_name == first_name
138 | assert obj.last_name == last_name
139 |
140 | def test_find_one_none(self):
141 | collection = self.db.collection(f.word())
142 | first_name = f.first_name()
143 | last_name = f.last_name()
144 |
145 | # Add others
146 | self.create_junk_document(collection)
147 |
148 | # Find
149 | obj = collection.find_one(first_name=first_name, last_name=last_name)
150 | assert obj is None
151 |
152 | def test_find_one_with_kwargs_from_empty_collection(self):
153 | collection = self.db.collection(f.word())
154 |
155 | # Find
156 | obj = collection.find_one(first_name=f.first_name(), last_name=f.last_name())
157 | assert obj is None
158 |
159 | def test_find_one_without_kwargs_from_empty_collection(self):
160 | collection = self.db.collection(f.word())
161 |
162 | # Find
163 | obj = collection.find_one()
164 | assert obj is None
165 |
166 | # First
167 | def test_first_when_its_first(self):
168 | collection = self.db.collection(f.word())
169 | first_name = f.first_name()
170 | last_name = f.last_name()
171 |
172 | # Insert with specific names
173 | collection.insert_one(first_name=first_name, last_name=last_name)
174 |
175 | # Add others
176 | self.create_junk_document(collection)
177 |
178 | # Find
179 | obj = collection.first(first_name=first_name, last_name=last_name)
180 | assert isinstance(obj, PantherDocument)
181 | assert obj.first_name == first_name
182 | assert obj.last_name == last_name
183 |
184 | def test_first_of_many_finds(self):
185 | collection = self.db.collection(f.word())
186 | first_name = f.first_name()
187 | last_name = f.last_name()
188 |
189 | # Insert with specific names
190 | expected = collection.insert_one(first_name=first_name, last_name=last_name)
191 | collection.insert_one(first_name=first_name, last_name=last_name)
192 | collection.insert_one(first_name=first_name, last_name=last_name)
193 |
194 | # Add others
195 | self.create_junk_document(collection)
196 |
197 | # Find
198 | obj = collection.first(first_name=first_name, last_name=last_name)
199 | assert isinstance(obj, PantherDocument)
200 | assert obj.id == expected.id
201 |
202 | def test_first_when_its_last(self):
203 | collection = self.db.collection(f.word())
204 | first_name = f.first_name()
205 | last_name = f.last_name()
206 |
207 | # Add others
208 | self.create_junk_document(collection)
209 |
210 | # Insert with specific names
211 | expected = collection.insert_one(first_name=first_name, last_name=last_name)
212 |
213 | # Find
214 | obj = collection.first(first_name=first_name, last_name=last_name)
215 | assert isinstance(obj, PantherDocument)
216 | assert obj.first_name == first_name
217 | assert obj.last_name == last_name
218 | assert obj.id == expected.id
219 |
220 | def test_first_none(self):
221 | collection = self.db.collection(f.word())
222 | first_name = f.first_name()
223 | last_name = f.last_name()
224 |
225 | # Add others
226 | self.create_junk_document(collection)
227 |
228 | # Find
229 | obj = collection.first(first_name=first_name, last_name=last_name)
230 | assert obj is None
231 |
232 | def test_first_with_kwargs_from_empty_collection(self):
233 | collection = self.db.collection(f.word())
234 |
235 | # Find
236 | obj = collection.first(first_name=f.first_name(), last_name=f.last_name())
237 | assert obj is None
238 |
239 | def test_first_without_kwargs_from_empty_collection(self):
240 | collection = self.db.collection(f.word())
241 |
242 | # Find
243 | obj = collection.first()
244 | assert obj is None
245 |
246 | # Last
247 | def test_last_when_its_first(self):
248 | collection = self.db.collection(f.word())
249 | first_name = f.first_name()
250 | last_name = f.last_name()
251 |
252 | # Insert with specific names
253 | collection.insert_one(first_name=first_name, last_name=last_name)
254 |
255 | # Add others
256 | self.create_junk_document(collection)
257 |
258 | # Find
259 | obj = collection.first(first_name=first_name, last_name=last_name)
260 | assert isinstance(obj, PantherDocument)
261 | assert obj.first_name == first_name
262 | assert obj.last_name == last_name
263 |
264 | def test_last_of_many_finds(self):
265 | collection = self.db.collection(f.word())
266 | first_name = f.first_name()
267 | last_name = f.last_name()
268 |
269 | # Insert with specific names
270 | collection.insert_one(first_name=first_name, last_name=last_name)
271 | collection.insert_one(first_name=first_name, last_name=last_name)
272 | expected = collection.insert_one(first_name=first_name, last_name=last_name)
273 |
274 | # Add others
275 | self.create_junk_document(collection)
276 |
277 | # Find
278 | obj = collection.last(first_name=first_name, last_name=last_name)
279 | assert isinstance(obj, PantherDocument)
280 | assert obj.id == expected.id
281 |
282 | def test_last_when_its_last(self):
283 | collection = self.db.collection(f.word())
284 | first_name = f.first_name()
285 | last_name = f.last_name()
286 |
287 | # Add others
288 | self.create_junk_document(collection)
289 |
290 | # Insert with specific names
291 | expected = collection.insert_one(first_name=first_name, last_name=last_name)
292 |
293 | # Find
294 | obj = collection.last(first_name=first_name, last_name=last_name)
295 | assert isinstance(obj, PantherDocument)
296 | assert obj.first_name == first_name
297 | assert obj.last_name == last_name
298 | assert obj.id == expected.id
299 |
300 | def test_last_none(self):
301 | collection = self.db.collection(f.word())
302 | first_name = f.first_name()
303 | last_name = f.last_name()
304 |
305 | # Add others
306 | self.create_junk_document(collection)
307 |
308 | # Find
309 | obj = collection.last(first_name=first_name, last_name=last_name)
310 | assert obj is None
311 |
312 | def test_last_with_kwargs_from_empty_collection(self):
313 | collection = self.db.collection(f.word())
314 |
315 | # Find
316 | obj = collection.last(first_name=f.first_name(), last_name=f.last_name())
317 | assert obj is None
318 |
319 | def test_last_without_kwargs_from_empty_collection(self):
320 | collection = self.db.collection(f.word())
321 |
322 | # Find
323 | obj = collection.last()
324 | assert obj is None
325 |
326 | # Find
327 | def test_find_response_type(self):
328 | collection = self.db.collection(f.word())
329 | first_name = f.first_name()
330 | collection.insert_one(first_name=first_name, last_name=f.last_name())
331 |
332 | # Find
333 | objs = collection.find(first_name=first_name)
334 |
335 | assert isinstance(objs, list)
336 | assert len(objs) == 1
337 | assert isinstance(objs[0], PantherDocument)
338 |
339 | def test_find_with_filter(self):
340 | collection = self.db.collection(f.word())
341 |
342 | # Add others
343 | self.create_junk_document(collection)
344 |
345 | # Insert with specific names
346 | first_name = f.first_name()
347 | _count = f.random.randint(2, 10)
348 | for i in range(_count):
349 | collection.insert_one(first_name=first_name, last_name=f.last_name())
350 |
351 | # Find
352 | objs = collection.find(first_name=first_name)
353 |
354 | assert isinstance(objs, list)
355 | assert len(objs) == _count
356 | for i in range(_count):
357 | assert objs[i].first_name == first_name
358 |
359 | def test_find_without_filter(self):
360 | collection = self.db.collection(f.word())
361 |
362 | # Add others
363 | _count_1 = self.create_junk_document(collection)
364 |
365 | # Insert with specific names
366 | first_name = f.first_name()
367 | _count_2 = f.random.randint(2, 10)
368 | for i in range(_count_2):
369 | collection.insert_one(first_name=first_name, last_name=f.last_name())
370 |
371 | # Find
372 | objs = collection.find()
373 | _count_all = _count_1 + _count_2
374 |
375 | assert isinstance(objs, list)
376 | assert len(objs) == _count_all
377 | for i in range(_count_all):
378 | assert isinstance(objs[i], PantherDocument)
379 |
380 | # Check count of specific name
381 | specific_count = 0
382 | for i in range(_count_all):
383 | if objs[i].first_name == first_name:
384 | specific_count += 1
385 |
386 | assert specific_count == _count_2
387 |
388 | # Count
389 | def test_count_with_filter(self):
390 | collection = self.db.collection(f.word())
391 |
392 | # Add others
393 | _count_1 = self.create_junk_document(collection)
394 |
395 | # Insert with specific names
396 | first_name = f.first_name()
397 | _count_2 = f.random.randint(2, 10)
398 | for i in range(_count_2):
399 | collection.insert_one(first_name=first_name, last_name=f.last_name())
400 |
401 | count_specific = collection.count(first_name=first_name)
402 | assert count_specific == _count_2
403 | assert count_specific == len(collection.find(first_name=first_name))
404 |
405 | # Delete Self
406 | def test_delete(self):
407 | collection = self.db.collection(f.word())
408 |
409 | # Add others
410 | _count = self.create_junk_document(collection)
411 |
412 | # Insert with specific name
413 | first_name = f.first_name()
414 | collection.insert_one(first_name=first_name, last_name=f.last_name())
415 |
416 | # Find
417 | obj = collection.find_one(first_name=first_name)
418 |
419 | # Delete It
420 | obj.delete()
421 |
422 | # Find It Again
423 | new_obj = collection.find_one(first_name=first_name)
424 | assert new_obj is None
425 |
426 | # Count of all
427 | objs_count = collection.count()
428 | assert objs_count == _count
429 |
430 | # Delete One
431 | def test_delete_one(self):
432 | collection = self.db.collection(f.word())
433 |
434 | # Add others
435 | _count = self.create_junk_document(collection)
436 |
437 | first_name = f.first_name()
438 | collection.insert_one(first_name=first_name, last_name=f.last_name())
439 |
440 | # Delete One
441 | is_deleted = collection.delete_one(first_name=first_name)
442 | assert is_deleted is True
443 |
444 | # Find It Again
445 | new_obj = collection.find_one(first_name=first_name)
446 | assert new_obj is None
447 |
448 | # Count of all
449 | objs_count = collection.count()
450 | assert objs_count == _count
451 |
452 | def test_delete_one_not_found(self):
453 | collection = self.db.collection(f.word())
454 |
455 | # Add others
456 | _count = self.create_junk_document(collection)
457 |
458 | first_name = f.first_name()
459 |
460 | # Delete One
461 | is_deleted = collection.delete_one(first_name=first_name)
462 | assert is_deleted is False
463 |
464 | # Count of all
465 | objs_count = collection.count()
466 | assert objs_count == _count
467 |
468 | def test_delete_one_first(self):
469 | collection = self.db.collection(f.word())
470 |
471 | # Add others
472 | _count_1 = self.create_junk_document(collection)
473 |
474 | first_name = f.first_name()
475 | _count_2 = f.random.randint(2, 10)
476 | for i in range(_count_2):
477 | collection.insert_one(first_name=first_name, last_name=f.last_name())
478 |
479 | # Delete One
480 | is_deleted = collection.delete_one(first_name=first_name)
481 | assert is_deleted is True
482 |
483 | # Count of all
484 | objs_count = collection.count()
485 | assert objs_count == (_count_1 + _count_2 - 1)
486 |
487 | # Count of undeleted
488 | undeleted_count = collection.count(first_name=first_name)
489 | assert undeleted_count == (_count_2 - 1)
490 |
491 | # Delete Many
492 | def test_delete_many(self):
493 | collection = self.db.collection(f.word())
494 |
495 | # Add others
496 | _count_1 = self.create_junk_document(collection)
497 |
498 | # Insert with specific name
499 | first_name = f.first_name()
500 | _count_2 = f.random.randint(2, 10)
501 | for i in range(_count_2):
502 | collection.insert_one(first_name=first_name, last_name=f.last_name())
503 |
504 | # Delete Many
505 | deleted_count = collection.delete_many(first_name=first_name)
506 | assert deleted_count == _count_2
507 |
508 | # Count of all
509 | objs_count = collection.count()
510 | assert objs_count == _count_1
511 |
512 | def test_delete_many_not_found(self):
513 | collection = self.db.collection(f.word())
514 |
515 | # Add others
516 | _count = self.create_junk_document(collection)
517 |
518 | first_name = f.first_name()
519 |
520 | # Delete Many
521 | deleted_count = collection.delete_many(first_name=first_name)
522 | assert deleted_count == 0
523 |
524 | # Count of all
525 | objs_count = collection.count()
526 | assert objs_count == _count
527 |
528 | # Update Self
529 | def test_update(self):
530 | collection = self.db.collection(f.word())
531 |
532 | # Add others
533 | _count = self.create_junk_document(collection)
534 |
535 | # Insert with specific name
536 | first_name = f.first_name()
537 | collection.insert_one(first_name=first_name, last_name=f.last_name())
538 |
539 | # Find One
540 | obj = collection.find_one(first_name=first_name)
541 | new_name = f.first_name()
542 | obj.update(first_name=new_name)
543 | assert obj.first_name == new_name
544 |
545 | # Find with old name
546 | old_obj = collection.find_one(first_name=first_name)
547 | assert old_obj is None
548 |
549 | # Find with new name
550 | obj = collection.find_one(first_name=new_name)
551 | assert obj.first_name == new_name
552 |
553 | # Update One
554 | def test_update_one_single_document(self):
555 | collection = self.db.collection(f.word())
556 |
557 | # Add others
558 | _count = self.create_junk_document(collection)
559 |
560 | # Insert with specific name
561 | first_name = f.first_name()
562 | collection.insert_one(first_name=first_name, last_name=f.last_name())
563 |
564 | # Update One
565 | new_name = f.first_name()
566 | is_updated = collection.update_one({'first_name': first_name}, first_name=new_name)
567 | assert is_updated is True
568 |
569 | # Find with old name
570 | old_obj = collection.find_one(first_name=first_name)
571 | assert old_obj is None
572 |
573 | # Find with new name
574 | obj = collection.find_one(first_name=new_name)
575 | assert obj.first_name == new_name
576 |
577 | def test_update_one_single_document_not_found(self):
578 | collection = self.db.collection(f.word())
579 |
580 | # Add others
581 | _count = self.create_junk_document(collection)
582 |
583 | # Insert with specific name
584 | first_name = f.first_name()
585 | collection.insert_one(first_name=first_name, last_name=f.last_name())
586 |
587 | # Update One
588 | new_name = f.first_name()
589 | is_updated = collection.update_one({'first_name': f.first_name()}, first_name=new_name)
590 | assert is_updated is False
591 |
592 | # Find with old name
593 | old_obj = collection.find_one(first_name=first_name)
594 | assert old_obj is not None
595 |
596 | # Find with new name
597 | obj = collection.find_one(first_name=new_name)
598 | assert obj is None
599 |
600 | # Update Many
601 | def test_update_many(self):
602 | collection = self.db.collection(f.word())
603 |
604 | # Add others
605 | _count_1 = self.create_junk_document(collection)
606 |
607 | # Insert with specific name
608 | first_name = f.first_name()
609 | _count_2 = f.random.randint(2, 10)
610 | for i in range(_count_2):
611 | collection.insert_one(first_name=first_name, last_name=f.last_name())
612 |
613 | # Update Many
614 | new_name = f.first_name()
615 | updated_count = collection.update_many({'first_name': first_name}, first_name=new_name)
616 | assert updated_count == _count_2
617 |
618 | # Find Them with old name
619 | objs = collection.find(first_name=first_name)
620 | assert objs == []
621 |
622 | # Find Them with new name
623 | objs = collection.find(first_name=new_name)
624 | assert len(objs) == _count_2
625 |
626 | # Count of all
627 | objs_count = collection.count()
628 | assert objs_count == (_count_1 + _count_2)
629 |
630 | # Fields
631 | def test_document_fields(self):
632 | collection = self.db.collection(f.word())
633 | first_name = f.first_name()
634 | last_name = f.last_name()
635 |
636 | # Insert with specific names
637 | collection.insert_one(first_name=first_name, last_name=last_name)
638 |
639 | # Find
640 | obj = collection.find_one(first_name=first_name, last_name=last_name)
641 |
642 | assert set(obj.data.keys()) == {'first_name', 'last_name', '_id'}
643 |
644 | # Save
645 | def test_document_save_method(self):
646 | collection = self.db.collection(f.word())
647 |
648 | # Insert with specific name
649 | first_name = f.first_name()
650 | collection.insert_one(first_name=first_name, last_name=f.last_name())
651 |
652 | # Find One
653 | obj = collection.find_one(first_name=first_name)
654 | new_name = f.first_name()
655 |
656 | # Update it
657 | obj.first_name = new_name
658 | obj.save()
659 |
660 | assert obj.first_name == new_name
661 |
662 | # Find with old name
663 | old_obj = collection.find_one(first_name=first_name)
664 | assert old_obj is None
665 |
666 | # Find with new name
667 | obj = collection.find_one(first_name=new_name)
668 | assert obj.first_name == new_name
669 |
670 | # Json
671 | def test_document_json_method(self):
672 | collection = self.db.collection(f.word())
673 | first_name = f.first_name()
674 | last_name = f.last_name()
675 |
676 | # Insert with specific names
677 | collection.insert_one(first_name=first_name, last_name=last_name)
678 |
679 | # Find
680 | obj = collection.find_one(first_name=first_name, last_name=last_name)
681 |
682 | _json = {
683 | 'first_name': first_name,
684 | 'last_name': last_name,
685 | '_id': obj.id,
686 | }
687 | assert obj.json() == json.dumps(_json).decode()
688 |
689 | class TestCursorPantherDB(IsolatedAsyncioTestCase):
690 |
691 | @classmethod
692 | def setUp(cls):
693 | cls.db_name = uuid4().hex
694 | cls.db_name = f'{cls.db_name}.pdb'
695 | cls.db = PantherDB(db_name=cls.db_name, return_cursor=True)
696 |
697 | @classmethod
698 | def tearDown(cls):
699 | Path(cls.db_name).unlink()
700 |
701 | @classmethod
702 | def create_junk_document(cls, collection) -> int:
703 | _count = f.random.randint(2, 10)
704 | for i in range(_count):
705 | collection.insert_one(first_name=f'{f.first_name()}{i}', last_name=f'{f.last_name()}{i}')
706 | return _count
707 |
708 | # Find
709 | def test_find_response_type(self):
710 | collection = self.db.collection(f.word())
711 | first_name = f.first_name()
712 | collection.insert_one(first_name=first_name, last_name=f.last_name())
713 |
714 | # Find
715 | objs = collection.find(first_name=first_name)
716 |
717 | assert isinstance(objs, Cursor)
718 | assert len([o for o in objs]) == 1
719 | assert isinstance(objs[0], PantherDocument)
720 |
721 | def test_find_with_filter(self):
722 | collection = self.db.collection(f.word())
723 |
724 | # Add others
725 | self.create_junk_document(collection)
726 |
727 | # Insert with specific names
728 | first_name = f.first_name()
729 | _count = f.random.randint(2, 10)
730 | last_names = []
731 | for i in range(_count):
732 | last_name = f.last_name()
733 | last_names.append(last_name)
734 | collection.insert_one(first_name=first_name, last_name=last_name)
735 |
736 | # Find
737 | objs = collection.find(first_name=first_name)
738 |
739 | assert isinstance(objs, Cursor)
740 | assert len([o for o in objs]) == _count
741 | for i in range(_count):
742 | assert objs[i].first_name == first_name
743 | assert objs[i].last_name == last_names[i]
744 |
745 | def test_find_without_filter(self):
746 | collection = self.db.collection(f.word())
747 |
748 | # Add others
749 | _count_1 = self.create_junk_document(collection)
750 |
751 | # Insert with specific names
752 | first_name = f.first_name()
753 | _count_2 = f.random.randint(2, 10)
754 | for i in range(_count_2):
755 | collection.insert_one(first_name=first_name, last_name=f.last_name())
756 |
757 | # Find
758 | objs = collection.find()
759 | _count_all = _count_1 + _count_2
760 |
761 | assert isinstance(objs, Cursor)
762 | assert len([o for o in objs]) == _count_all
763 | for i in range(_count_all):
764 | assert isinstance(objs[i], PantherDocument)
765 |
766 | # Check count of specific name
767 | specific_count = 0
768 | for i in range(_count_all):
769 | if objs[i].first_name == first_name:
770 | specific_count += 1
771 |
772 | assert specific_count == _count_2
773 |
774 | def test_find_with_sort(self):
775 | collection = self.db.collection(f.word())
776 |
777 | # Insert with specific values
778 | collection.insert_one(first_name='A', last_name=0)
779 | collection.insert_one(first_name='A', last_name=1)
780 | collection.insert_one(first_name='B', last_name=0)
781 | collection.insert_one(first_name='B', last_name=1)
782 |
783 | # Find without sort
784 | objs = collection.find()
785 | assert (objs[0].first_name, objs[0].last_name) == ('A', 0)
786 | assert (objs[1].first_name, objs[1].last_name) == ('A', 1)
787 | assert (objs[2].first_name, objs[2].last_name) == ('B', 0)
788 | assert (objs[3].first_name, objs[3].last_name) == ('B', 1)
789 |
790 | # Find with single sort
791 | objs = collection.find().sort('first_name', 1)
792 | assert (objs[0].first_name, objs[0].last_name) == ('A', 0)
793 | assert (objs[1].first_name, objs[1].last_name) == ('A', 1)
794 | assert (objs[2].first_name, objs[2].last_name) == ('B', 0)
795 | assert (objs[3].first_name, objs[3].last_name) == ('B', 1)
796 |
797 | # Find with single sort as a list
798 | objs = collection.find().sort([('first_name', 1)])
799 | assert (objs[0].first_name, objs[0].last_name) == ('A', 0)
800 | assert (objs[1].first_name, objs[1].last_name) == ('A', 1)
801 | assert (objs[2].first_name, objs[2].last_name) == ('B', 0)
802 | assert (objs[3].first_name, objs[3].last_name) == ('B', 1)
803 |
804 | objs = collection.find().sort([('first_name', -1)])
805 | assert (objs[0].first_name, objs[0].last_name) == ('B', 0)
806 | assert (objs[1].first_name, objs[1].last_name) == ('B', 1)
807 | assert (objs[2].first_name, objs[2].last_name) == ('A', 0)
808 | assert (objs[3].first_name, objs[3].last_name) == ('A', 1)
809 |
810 | objs = collection.find().sort([('last_name', 1)])
811 | assert (objs[0].first_name, objs[0].last_name) == ('A', 0)
812 | assert (objs[1].first_name, objs[1].last_name) == ('B', 0)
813 | assert (objs[2].first_name, objs[2].last_name) == ('A', 1)
814 | assert (objs[3].first_name, objs[3].last_name) == ('B', 1)
815 |
816 | objs = collection.find().sort([('last_name', -1)])
817 | assert (objs[0].first_name, objs[0].last_name) == ('A', 1)
818 | assert (objs[1].first_name, objs[1].last_name) == ('B', 1)
819 | assert (objs[2].first_name, objs[2].last_name) == ('A', 0)
820 | assert (objs[3].first_name, objs[3].last_name) == ('B', 0)
821 |
822 | # Find with multiple sort
823 | objs = collection.find().sort([('first_name', 1), ('last_name', 1)])
824 | assert (objs[0].first_name, objs[0].last_name) == ('A', 0)
825 | assert (objs[1].first_name, objs[1].last_name) == ('A', 1)
826 | assert (objs[2].first_name, objs[2].last_name) == ('B', 0)
827 | assert (objs[3].first_name, objs[3].last_name) == ('B', 1)
828 |
829 | objs = collection.find().sort([('first_name', 1), ('last_name', -1)])
830 | assert (objs[0].first_name, objs[0].last_name) == ('A', 1)
831 | assert (objs[1].first_name, objs[1].last_name) == ('A', 0)
832 | assert (objs[2].first_name, objs[2].last_name) == ('B', 1)
833 | assert (objs[3].first_name, objs[3].last_name) == ('B', 0)
834 |
835 | objs = collection.find().sort([('first_name', -1), ('last_name', 1)])
836 | assert (objs[0].first_name, objs[0].last_name) == ('B', 0)
837 | assert (objs[1].first_name, objs[1].last_name) == ('B', 1)
838 | assert (objs[2].first_name, objs[2].last_name) == ('A', 0)
839 | assert (objs[3].first_name, objs[3].last_name) == ('A', 1)
840 |
841 | objs = collection.find().sort([('first_name', -1), ('last_name', -1)])
842 | assert (objs[0].first_name, objs[0].last_name) == ('B', 1)
843 | assert (objs[1].first_name, objs[1].last_name) == ('B', 0)
844 | assert (objs[2].first_name, objs[2].last_name) == ('A', 1)
845 | assert (objs[3].first_name, objs[3].last_name) == ('A', 0)
846 |
847 | async def test_find_iterations(self):
848 | collection = self.db.collection(f.word())
849 |
850 | # Insert with specific values
851 | collection.insert_one(first_name='A', last_name=0)
852 | collection.insert_one(first_name='A', last_name=1)
853 | collection.insert_one(first_name='B', last_name=0)
854 | collection.insert_one(first_name='B', last_name=1)
855 |
856 | # Find without sort
857 | expected_without_sort_data = {
858 | 0: ('A', 0),
859 | 1: ('A', 1),
860 | 2: ('B', 0),
861 | 3: ('B', 1),
862 | }
863 | objs = collection.find()
864 | for i, obj in enumerate(objs):
865 | assert (obj.first_name, obj.last_name) == expected_without_sort_data[i]
866 |
867 | i = 0
868 | async_objs = collection.find()
869 | async for obj in async_objs:
870 | assert (obj.first_name, obj.last_name) == expected_without_sort_data[i]
871 | i += 1
872 |
873 | # # Find Single sort
874 | expected_single_sort_data = {
875 | 0: ('B', 0),
876 | 1: ('B', 1),
877 | 2: ('A', 0),
878 | 3: ('A', 1),
879 | }
880 | objs = collection.find().sort('first_name', -1)
881 | for i, obj in enumerate(objs):
882 | assert (obj.first_name, obj.last_name) == expected_single_sort_data[i]
883 |
884 | i = 0
885 | async_objs = collection.find().sort('first_name', -1)
886 | async for obj in async_objs:
887 | assert (obj.first_name, obj.last_name) == expected_single_sort_data[i]
888 | i += 1
889 |
890 |
891 |
892 | # TODO: Test whole scenario with -> secret_key, return_dict
893 | # TODO: Test where exceptions happen
894 |
--------------------------------------------------------------------------------