├── .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 | [![PyPI](https://img.shields.io/pypi/v/pantherdb?label=PyPI)](https://pypi.org/project/pantherdb/) [![PyVersion](https://img.shields.io/pypi/pyversions/pantherdb.svg)](https://pypi.org/project/pantherdb/) [![Downloads](https://static.pepy.tech/badge/pantherdb/month)](https://pepy.tech/project/pantherdb) [![license](https://img.shields.io/github/license/alirn76/pantherdb.svg)](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 | --------------------------------------------------------------------------------