├── .github └── workflows │ └── python-app.yml ├── .gitignore ├── LICENSE ├── README.md ├── __init__.py ├── dist ├── python-redis-orm-0.1.0.tar.gz ├── python-redis-orm-0.1.1.tar.gz ├── python-redis-orm-0.1.2.tar.gz ├── python-redis-orm-0.1.3.tar.gz ├── python-redis-orm-0.1.4.tar.gz ├── python-redis-orm-0.1.7.tar.gz ├── python-redis-orm-0.1.8.tar.gz ├── python-redis-orm-0.1.9.tar.gz ├── python-redis-orm-0.2.0.tar.gz ├── python-redis-orm-0.2.1.tar.gz ├── python-redis-orm-0.2.2.tar.gz ├── python-redis-orm-0.2.3.tar.gz ├── python-redis-orm-0.2.4.tar.gz ├── python-redis-orm-0.2.5.tar.gz ├── python-redis-orm-0.2.7.tar.gz ├── python-redis-orm-0.2.8.tar.gz ├── python-redis-orm-0.2.9.tar.gz ├── python-redis-orm-0.3.0.tar.gz ├── python-redis-orm-0.3.1.tar.gz ├── python-redis-orm-0.3.2.tar.gz ├── python-redis-orm-0.3.3.tar.gz ├── python-redis-orm-0.3.4.tar.gz ├── python-redis-orm-0.3.5.tar.gz ├── python-redis-orm-0.3.6.tar.gz ├── python-redis-orm-0.3.7.tar.gz ├── python-redis-orm-0.3.8.tar.gz ├── python-redis-orm-0.3.9.tar.gz ├── python-redis-orm-0.4.0.tar.gz ├── python-redis-orm-0.4.1.tar.gz ├── python-redis-orm-0.4.2.tar.gz ├── python-redis-orm-0.4.3.tar.gz ├── python-redis-orm-0.4.4.tar.gz ├── python-redis-orm-0.4.5.tar.gz ├── python-redis-orm-0.4.6.tar.gz ├── python-redis-orm-0.4.7.tar.gz ├── python-redis-orm-0.4.8.tar.gz ├── python-redis-orm-0.4.9.tar.gz ├── python-redis-orm-0.5.0.tar.gz ├── python-redis-orm-0.5.1.tar.gz ├── python-redis-orm-0.5.2.tar.gz ├── python-redis-orm-0.5.3.tar.gz ├── python-redis-orm-0.5.4.tar.gz ├── python_redis_orm-0.1.0-py3-none-any.whl ├── python_redis_orm-0.1.1-py3-none-any.whl ├── python_redis_orm-0.1.2-py3-none-any.whl ├── python_redis_orm-0.1.3-py3-none-any.whl ├── python_redis_orm-0.1.4-py3-none-any.whl ├── python_redis_orm-0.1.4.tar.gz ├── python_redis_orm-0.1.5-py3-none-any.whl ├── python_redis_orm-0.1.5.tar.gz ├── python_redis_orm-0.1.6-py3-none-any.whl ├── python_redis_orm-0.1.6.tar.gz ├── python_redis_orm-0.1.7-py3-none-any.whl ├── python_redis_orm-0.1.8-py3-none-any.whl ├── python_redis_orm-0.1.9-py3-none-any.whl ├── python_redis_orm-0.2.0-py3-none-any.whl ├── python_redis_orm-0.2.1-py3-none-any.whl ├── python_redis_orm-0.2.2-py3-none-any.whl ├── python_redis_orm-0.2.3-py3-none-any.whl ├── python_redis_orm-0.2.4-py3-none-any.whl ├── python_redis_orm-0.2.5-py3-none-any.whl ├── python_redis_orm-0.2.7-py3-none-any.whl ├── python_redis_orm-0.2.8-py3-none-any.whl ├── python_redis_orm-0.2.9-py3-none-any.whl ├── python_redis_orm-0.3.0-py3-none-any.whl ├── python_redis_orm-0.3.1-py3-none-any.whl ├── python_redis_orm-0.3.2-py3-none-any.whl ├── python_redis_orm-0.3.3-py3-none-any.whl ├── python_redis_orm-0.3.4-py3-none-any.whl ├── python_redis_orm-0.3.5-py3-none-any.whl ├── python_redis_orm-0.3.6-py3-none-any.whl ├── python_redis_orm-0.3.7-py3-none-any.whl ├── python_redis_orm-0.3.8-py3-none-any.whl ├── python_redis_orm-0.3.9-py3-none-any.whl ├── python_redis_orm-0.4.0-py3-none-any.whl ├── python_redis_orm-0.4.1-py3-none-any.whl ├── python_redis_orm-0.4.2-py3-none-any.whl ├── python_redis_orm-0.4.3-py3-none-any.whl ├── python_redis_orm-0.4.4-py3-none-any.whl ├── python_redis_orm-0.4.5-py3-none-any.whl ├── python_redis_orm-0.4.6-py3-none-any.whl ├── python_redis_orm-0.4.7-py3-none-any.whl ├── python_redis_orm-0.4.8-py3-none-any.whl ├── python_redis_orm-0.4.9-py3-none-any.whl ├── python_redis_orm-0.5.0-py3-none-any.whl ├── python_redis_orm-0.5.1-py3-none-any.whl ├── python_redis_orm-0.5.2-py3-none-any.whl ├── python_redis_orm-0.5.3-py3-none-any.whl └── python_redis_orm-0.5.4-py3-none-any.whl ├── pyproject.toml └── python_redis_orm ├── __init__.py ├── core.py ├── tests └── full_test.py └── utils.py /.github/workflows/python-app.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Full test 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | services: 18 | # Label used to access the service container 19 | redis: 20 | # Docker Hub image 21 | image: redis 22 | # Set health checks to wait until redis has started 23 | options: >- 24 | --health-cmd "redis-cli ping" 25 | --health-interval 10s 26 | --health-timeout 5s 27 | --health-retries 5 28 | ports: 29 | # Maps port 6379 on service container to the host 30 | - 6379:6379 31 | 32 | steps: 33 | - uses: actions/checkout@v2 34 | - name: Set up Python 3.9 35 | uses: actions/setup-python@v2 36 | with: 37 | python-version: 3.9 38 | - name: Install dependencies 39 | run: | 40 | python -m pip install --upgrade pip 41 | pip install -U python-redis-orm 42 | pip install flake8 pytest 43 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 44 | - name: Lint with flake8 45 | run: | 46 | # stop the build if there are Python syntax errors or undefined names 47 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 48 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 49 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 50 | - name: Run full test 51 | run: | 52 | python python_redis_orm/tests/full_test.py 53 | env: 54 | # The hostname used to communicate with the Redis service container 55 | REDIS_HOST: localhost 56 | # The default Redis port 57 | REDIS_PORT: 6379 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | /.idea/ 3 | /venv/ 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 gh0st-work 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 | # python-redis-orm 2 | 3 | ## **Python Redis ORM library that gives redis easy-to-use objects with fields and speeds a development up, inspired by Django ORM** 4 | 5 | 6 | [![Full test](https://github.com/gh0st-work/python_redis_orm/actions/workflows/python-app.yml/badge.svg?event=push)](https://github.com/gh0st-work/python_redis_orm/actions/workflows/python-app.yml) 7 | 8 | For one project, I needed to work with redis, but redis-py provides a minimum level of work with redis. I didn't find any Django-like ORM for redis, so I wrote this library, then there will be a port to Django. 9 | 10 | ### Working with this library, you are expected: 11 | 12 | - Fully works in 2021 13 | - Django-like architecture 14 | - Easy adaptation to your needs 15 | - Adequate informational messages and error messages 16 | - Built-in RedisRoot class that stores specified models, with: 17 | - **redis_instance** setting - your redis connection (from redis-py) 18 | - **prefix** setting - prefix of this RedisRoot to be stored in redis 19 | - **ignore_deserialization_errors** setting - do not raise errors, while deserializing data 20 | - **save_consistency** setting - show structure-first data 21 | - **economy** setting - to not return full data and save some requests (usually, speeds up your app on 80%) 22 | - 13 built-in types of fields: 23 | - **RedisField** - base class for nesting 24 | - **RedisString** - string 25 | - **RedisNumber** - int or float 26 | - **RedisId** - instances IDs 27 | - **RedisBool** - bool 28 | - **RedisDecimal** - working accurately with numbers via decimal 29 | - **RedisJson** - for data, that can be JSONed 30 | - **RedisList** - list 31 | - **RedisDict** - dict 32 | - **RedisDateTime** - for work with date and time, via python datetime.datetime 33 | - **RedisDate** - for work with date, via python datetime.data 34 | - **RedisForeignKey** - for link to other instance 35 | - **RedisManyToMany** - for links to other instances 36 | - All fields supports: 37 | - Automatically serialization 38 | - Automatically deserialization 39 | - TTL (Time To Live) setting 40 | - Default values 41 | - Providing functions without call, to call, while need 42 | - Allow null values setting 43 | - Choices 44 | - Filtering (and deep filtering): 45 | - **exact** - equality 46 | - **iexact** - case-independent equality 47 | - **contains** - is filter string in the value string 48 | - **icontains** - is filter string case-independent in the value string 49 | - **in** - is value in the provided list 50 | - **gt** - is value greater 51 | - **gte** - is value greater or equals 52 | - **lt** - is value less 53 | - **lte** - is value less or equals 54 | - **startswith** - is string starts with 55 | - **istartswith** - is string case-independent starts with 56 | - **endswith** - is string ends with 57 | - **iendswith** - is string case-independent ends wth 58 | - **range** - is value in provided range 59 | - **isnull** - is value in ["null", None] 60 | - Built-in RedisModel class, with: 61 | - All fields that you want 62 | - TTL (Time To Live) 63 | - CRUD (Create Read Update Delete) 64 | - Non-blocking usage! Any operation gives the same result as the default, but it just creates an asyncio task in the background instead of write inside the call 65 | 66 | 67 | # Installation 68 | `pip install python-redis-orm` 69 | 70 | [Here is PyPI](https://pypi.org/project/python-redis-orm/) 71 | 72 | Obviously, you need to install and run redis server on your machine, we support v3+ 73 | 74 | 75 | # Usage 76 | 77 | 1. Create **RedisRoot** with params: 78 | - **prefix** (str) - prefix for your redis root 79 | - **connection_pool** (redis.ConnectionPool) - redis-py redis.ConnectionPool instance, with decode_responses=True 80 | - **ignore_deserialization_errors** (bool) - to ignore deserialization errors or raise exception 81 | - **save_consistency** (bool) - to use structure-first data 82 | - **economy** (bool) - if True, all update requests will return only instance id 83 | - **use_keys** (bool) - to use Redis keys command (uses memory instead of CPU) instead of scan 84 | 2. Create your models 85 | 3. Call **register_models()** on your RedisRoot instance and provide list with your models 86 | 4. Use our CRUD 87 | 88 | 89 | # CRUD 90 | ```python 91 | example_instance = ExampleModel(example_field='example_data').save() # - to create an instance and get its data dict 92 | # or: 93 | example_instance = redis_root.create(ExampleModel, example_field='example_data') 94 | filtered_example_instances = redis_root.get(ExampleModel, example_field='example_data') # - to get all ExampleModel instances with example_field filter and get its data dict 95 | ordered_instances = redis_root.order(filtered_example_instances, '-id') # - to get ordered filtered_example_instances by id ('-' for reverse) 96 | updated_example_instances = redis_root.update(ExampleModel, ordered_instances, example_field='another_example_data') # - to update all ordered_instances example_field with value 'another_example_data' and get its data dict 97 | redis_root.delete(ExampleModel, updated_example_instances) # - to delete updated_example_instances 98 | 99 | # Non-blocking funcs are the same, just add "_nb" to the end: 100 | # ExampleModel(...).save_nb() 101 | # redis_root.create_nb(...) 102 | # redis_root.update_nb(...) 103 | # redis_root.delete_nb(...) 104 | 105 | ``` 106 | 107 | 108 | # Example usage 109 | 110 | All features: 111 | 112 | [full_test.py](https://github.com/gh0st-work/python_redis_orm/blob/master/python_redis_orm/tests/full_test.py) 113 | ```python 114 | import random 115 | import sys 116 | from time import sleep 117 | import asyncio 118 | import os 119 | 120 | from python_redis_orm.core import * 121 | 122 | 123 | def generate_token(chars_count): 124 | allowed_chars = 'QWERTYUIOPASDFGHJKLZXCVBNM1234567890' 125 | token = f'{"".join([random.choice(allowed_chars) for i in range(chars_count)])}' 126 | return token 127 | 128 | 129 | def generate_token_12_chars(): 130 | return generate_token(12) 131 | 132 | 133 | class BotSession(RedisModel): 134 | session_token = RedisString(default=generate_token_12_chars) 135 | created = RedisDateTime(default=datetime.datetime.now) 136 | 137 | 138 | class TaskChallenge(RedisModel): 139 | bot_session = RedisForeignKey(model=BotSession) 140 | task_id = RedisNumber(default=0, null=False) 141 | status = RedisString(default='in_work', choices={ 142 | 'in_work': 'В работе', 143 | 'completed': 'Завершён успешно', 144 | 'completed_frozen_points': 'Завершён успешно, получил поинты в холде', 145 | 'completed_points': 'Завершён успешно, получил поинты', 146 | 'completed_decommissioning': 'Завершён успешно, поинты списаны', 147 | 'failed_bot': 'Зафейлил бот', 148 | 'failed_task_creator': 'Зафейлил создатель задания', 149 | }, null=False) 150 | account_checks_count = RedisNumber(default=0) 151 | created = RedisDateTime(default=datetime.datetime.now) 152 | 153 | 154 | class TtlCheckModel(RedisModel): 155 | redis_number_with_ttl = RedisNumber(default=0, null=False) 156 | 157 | 158 | class MetaTtlCheckModel(RedisModel): 159 | redis_number = RedisNumber(default=0, null=False) 160 | 161 | class Meta: 162 | ttl = 5 163 | 164 | 165 | class DictCheckModel(RedisModel): 166 | redis_dict = RedisDict() 167 | 168 | 169 | class ListCheckModel(RedisModel): 170 | redis_list = RedisList() 171 | 172 | 173 | class ForeignKeyCheckModel(RedisModel): 174 | task_challenge = RedisForeignKey(model=TaskChallenge) 175 | 176 | 177 | class ManyToManyCheckModel(RedisModel): 178 | task_challenges = RedisManyToMany(model=TaskChallenge) 179 | 180 | 181 | class ModelWithOverriddenSave(RedisModel): 182 | multiplied_max_field = RedisNumber() 183 | 184 | def save(self): 185 | redis_root = self.get('redis_root') # get value of any field 186 | new_value = 1 187 | all_instances = redis_root.get(ModelWithOverriddenSave) 188 | if all_instances: 189 | max_value = max(list(map(lambda instance: instance['multiplied_max_field'], all_instances))) 190 | new_value = max_value * 2 191 | self.set(multiplied_max_field=new_value) 192 | return super().save() 193 | 194 | 195 | def clean_db_after_test(connection_pool, prefix): 196 | redis_instance = redis.Redis(connection_pool=connection_pool) 197 | for key in redis_instance.keys(f'{prefix}:*'): 198 | redis_instance.delete(key) 199 | 200 | 201 | def basic_test(connection_pool, prefix): 202 | try: 203 | redis_root = RedisRoot( 204 | prefix=prefix, 205 | connection_pool=connection_pool, 206 | ignore_deserialization_errors=True 207 | ) 208 | redis_root.register_models([ 209 | TaskChallenge, 210 | ]) 211 | for i in range(5): 212 | TaskChallenge( 213 | redis_root=redis_root, 214 | status='in_work', 215 | ).save() 216 | task_challenges_without_keys = redis_root.get(TaskChallenge) 217 | task_challenges_with_keys = redis_root.get(TaskChallenge, return_dict=True) 218 | have_exception = False 219 | if not len(task_challenges_without_keys): 220 | have_exception = True 221 | if not task_challenges_with_keys: 222 | have_exception = True 223 | else: 224 | if not task_challenges_with_keys.keys(): 225 | have_exception = True 226 | else: 227 | if len(list(task_challenges_with_keys.keys())) != len(task_challenges_without_keys): 228 | have_exception = True 229 | except BaseException as ex: 230 | have_exception = True 231 | clean_db_after_test(connection_pool, prefix) 232 | return have_exception 233 | 234 | 235 | def auto_reg_test(connection_pool, prefix): 236 | redis_root = RedisRoot( 237 | prefix=prefix, 238 | connection_pool=connection_pool, 239 | ignore_deserialization_errors=True 240 | ) 241 | task_challenge_1 = TaskChallenge( 242 | redis_root=redis_root, 243 | status='in_work', 244 | ).save() 245 | try: 246 | task_challenges = redis_root.get(TaskChallenge) 247 | have_exception = False 248 | except BaseException as ex: 249 | have_exception = True 250 | clean_db_after_test(connection_pool, prefix) 251 | return have_exception 252 | 253 | 254 | def no_connection_pool_test(*args, **kwargs): 255 | try: 256 | redis_root = RedisRoot( 257 | ignore_deserialization_errors=True 258 | ) 259 | task_challenge_1 = TaskChallenge( 260 | redis_root=redis_root, 261 | status='in_work', 262 | ) 263 | task_challenge_1.save() 264 | task_challenges = redis_root.get(TaskChallenge) 265 | have_exception = False 266 | connection_pool = redis.ConnectionPool( 267 | host=os.environ['REDIS_HOST'], 268 | port=os.environ['REDIS_PORT'], 269 | db=0, 270 | decode_responses=True 271 | ) 272 | clean_db_after_test(connection_pool, redis_root.prefix) 273 | except BaseException as ex: 274 | have_exception = True 275 | return have_exception 276 | 277 | 278 | def choices_test(connection_pool, prefix): 279 | redis_root = RedisRoot( 280 | prefix=prefix, 281 | connection_pool=connection_pool, 282 | ignore_deserialization_errors=True 283 | ) 284 | task_challenge_1 = TaskChallenge( 285 | redis_root=redis_root, 286 | status='bruh', 287 | ) 288 | try: 289 | save_result = task_challenge_1.save() 290 | task_challenges = redis_root.get(TaskChallenge) 291 | have_exception = True 292 | except BaseException as ex: 293 | have_exception = False 294 | clean_db_after_test(connection_pool, prefix) 295 | return have_exception 296 | 297 | 298 | def order_test(connection_pool, prefix): 299 | redis_root = RedisRoot( 300 | prefix=prefix, 301 | connection_pool=connection_pool, 302 | ignore_deserialization_errors=True 303 | ) 304 | for i in range(3): 305 | TaskChallenge( 306 | redis_root=redis_root 307 | ).save() 308 | have_exception = True 309 | try: 310 | task_challenges = redis_root.get(TaskChallenge) 311 | first_task_challenge = redis_root.order(task_challenges, 'id')[0] 312 | last_task_challenge = redis_root.order(task_challenges, '-id')[0] 313 | if first_task_challenge['id'] == 1 and last_task_challenge['id'] == len(task_challenges): 314 | have_exception = False 315 | except BaseException as ex: 316 | pass 317 | clean_db_after_test(connection_pool, prefix) 318 | return have_exception 319 | 320 | 321 | def filter_test(connection_pool, prefix): 322 | redis_root = RedisRoot( 323 | prefix=prefix, 324 | connection_pool=connection_pool, 325 | ignore_deserialization_errors=True 326 | ) 327 | have_exception = True 328 | try: 329 | same_tokens_count = 2 330 | random_tokens_count = 8 331 | same_token = generate_token(50) 332 | random_tokens = [generate_token(50) for i in range(random_tokens_count)] 333 | for i in range(same_tokens_count): 334 | BotSession(redis_root, session_token=same_token).save() 335 | for random_token in random_tokens: 336 | BotSession(redis_root, session_token=random_token).save() 337 | task_challenges_with_same_token = redis_root.get(BotSession, session_token=same_token) 338 | if len(task_challenges_with_same_token) == same_tokens_count: 339 | have_exception = False 340 | except BaseException as ex: 341 | print(ex) 342 | 343 | clean_db_after_test(connection_pool, prefix) 344 | return have_exception 345 | 346 | 347 | def update_test(connection_pool, prefix): 348 | redis_root = RedisRoot( 349 | prefix=prefix, 350 | connection_pool=connection_pool, 351 | ignore_deserialization_errors=True 352 | ) 353 | have_exception = True 354 | try: 355 | bot_session_1 = BotSession(redis_root, session_token='123').save() 356 | bot_session_1_id = bot_session_1['id'] 357 | redis_root.update(BotSession, bot_session_1, session_token='234') 358 | bot_sessions_filtered = redis_root.get(BotSession, id=bot_session_1_id) 359 | if len(bot_sessions_filtered) == 1: 360 | bot_session_1_new = bot_sessions_filtered[0] 361 | if 'session_token' in bot_session_1_new.keys(): 362 | if bot_session_1_new['session_token'] == '234': 363 | have_exception = False 364 | except BaseException as ex: 365 | print(ex) 366 | 367 | clean_db_after_test(connection_pool, prefix) 368 | return have_exception 369 | 370 | 371 | def functions_like_defaults_test(connection_pool, prefix): 372 | redis_root = RedisRoot( 373 | prefix=prefix, 374 | connection_pool=connection_pool, 375 | ignore_deserialization_errors=True 376 | ) 377 | have_exception = False 378 | try: 379 | bot_session_1 = BotSession(redis_root).save() 380 | bot_session_2 = BotSession(redis_root).save() 381 | if bot_session_1.session_token == bot_session_2.session_token: 382 | have_exception = True 383 | except BaseException as ex: 384 | pass 385 | 386 | clean_db_after_test(connection_pool, prefix) 387 | return have_exception 388 | 389 | 390 | def redis_foreign_key_test(connection_pool, prefix): 391 | redis_root = RedisRoot( 392 | prefix=prefix, 393 | connection_pool=connection_pool, 394 | ignore_deserialization_errors=True 395 | ) 396 | have_exception = True 397 | try: 398 | bot_session_1 = BotSession( 399 | redis_root=redis_root, 400 | ).save() 401 | task_challenge_1 = TaskChallenge( 402 | redis_root=redis_root, 403 | bot_session=bot_session_1 404 | ).save() 405 | bot_sessions = redis_root.get(BotSession) 406 | bot_session = redis_root.order(bot_sessions, '-id')[0] 407 | task_challenges = redis_root.get(TaskChallenge) 408 | task_challenge = redis_root.order(task_challenges, '-id')[0] 409 | if type(task_challenge['bot_session']) == dict: 410 | if task_challenge['bot_session'] == bot_session: 411 | have_exception = False 412 | except BaseException as ex: 413 | print(ex) 414 | 415 | clean_db_after_test(connection_pool, prefix) 416 | return have_exception 417 | 418 | 419 | def delete_test(connection_pool, prefix): 420 | redis_root = RedisRoot( 421 | prefix=prefix, 422 | connection_pool=connection_pool, 423 | ignore_deserialization_errors=True 424 | ) 425 | have_exception = True 426 | try: 427 | bot_session_1 = BotSession( 428 | redis_root=redis_root, 429 | ).save() 430 | task_challenge_1 = TaskChallenge( 431 | redis_root=redis_root, 432 | bot_session=bot_session_1 433 | ).save() 434 | redis_root.delete(BotSession, bot_session_1) 435 | redis_root.delete(TaskChallenge, task_challenge_1) 436 | bot_sessions = redis_root.get(BotSession) 437 | task_challenges = redis_root.get(TaskChallenge) 438 | if len(bot_sessions) == 0 and len(task_challenges) == 0: 439 | have_exception = False 440 | except BaseException as ex: 441 | print(ex) 442 | 443 | clean_db_after_test(connection_pool, prefix) 444 | return have_exception 445 | 446 | 447 | def save_consistency_test(connection_pool, prefix): 448 | redis_root = RedisRoot( 449 | prefix=prefix, 450 | connection_pool=connection_pool, 451 | ignore_deserialization_errors=True, 452 | save_consistency=True, 453 | ) 454 | have_exception = True 455 | try: 456 | ttl_check_model_1 = TtlCheckModel( 457 | redis_root=redis_root, 458 | ).save() 459 | ttl_check_models = redis_root.get(TtlCheckModel) 460 | if len(ttl_check_models): 461 | ttl_check_model = ttl_check_models[0] 462 | if 'redis_number_with_ttl' in ttl_check_model.keys(): 463 | sleep(6) 464 | ttl_check_models = redis_root.get(TtlCheckModel) 465 | if len(ttl_check_models): 466 | ttl_check_model = ttl_check_models[0] 467 | if 'redis_number_with_ttl' in ttl_check_model.keys(): # because consistency is saved 468 | have_exception = False 469 | except BaseException as ex: 470 | print(ex) 471 | 472 | clean_db_after_test(connection_pool, prefix) 473 | return have_exception 474 | 475 | 476 | def meta_ttl_test(connection_pool, prefix): 477 | redis_root = RedisRoot( 478 | prefix=prefix, 479 | connection_pool=connection_pool, 480 | ignore_deserialization_errors=True, 481 | ) 482 | have_exception = True 483 | try: 484 | meta_ttl_check_model_1 = MetaTtlCheckModel( 485 | redis_root=redis_root, 486 | ).save() 487 | meta_ttl_check_models = redis_root.get(MetaTtlCheckModel) 488 | if len(meta_ttl_check_models): 489 | meta_ttl_check_model = meta_ttl_check_models[0] 490 | if 'redis_number' in meta_ttl_check_model.keys(): 491 | sleep(6) 492 | meta_ttl_check_models = redis_root.get(MetaTtlCheckModel) 493 | if not len(meta_ttl_check_models): 494 | have_exception = False 495 | except BaseException as ex: 496 | print(ex) 497 | 498 | clean_db_after_test(connection_pool, prefix) 499 | return have_exception 500 | 501 | 502 | def use_keys_test(connection_pool, prefix): 503 | have_exception = True 504 | try: 505 | 506 | redis_root = RedisRoot( 507 | prefix=prefix, 508 | connection_pool=connection_pool, 509 | ignore_deserialization_errors=True, 510 | use_keys=True 511 | ) 512 | started_in_keys = datetime.datetime.now() 513 | tests_count = 100 514 | for i in range(tests_count): 515 | task_challenge_1 = TaskChallenge( 516 | redis_root=redis_root, 517 | status='in_work', 518 | ).save() 519 | redis_root.update(TaskChallenge, task_challenge_1, account_checks_count=1) 520 | ended_in_keys = datetime.datetime.now() 521 | keys_time = (ended_in_keys - started_in_keys).total_seconds() 522 | clean_db_after_test(connection_pool, prefix) 523 | 524 | redis_root = RedisRoot( 525 | prefix=prefix, 526 | connection_pool=connection_pool, 527 | ignore_deserialization_errors=True, 528 | use_keys=False 529 | ) 530 | started_in_no_keys = datetime.datetime.now() 531 | for i in range(tests_count): 532 | task_challenge_1 = TaskChallenge( 533 | redis_root=redis_root, 534 | status='in_work', 535 | ).save() 536 | redis_root.update(TaskChallenge, task_challenge_1, account_checks_count=1) 537 | ended_in_no_keys = datetime.datetime.now() 538 | no_keys_time = (ended_in_no_keys - started_in_no_keys).total_seconds() 539 | clean_db_after_test(connection_pool, prefix) 540 | keys_percent = round((no_keys_time / keys_time - 1) * 100, 2) 541 | keys_symbol = ('+' if keys_percent > 0 else '') 542 | print(f'Keys usage gives {keys_symbol}{keys_percent}% efficiency') 543 | have_exception = False 544 | except BaseException as ex: 545 | print(ex) 546 | 547 | clean_db_after_test(connection_pool, prefix) 548 | return have_exception 549 | 550 | 551 | def dict_test(connection_pool, prefix): 552 | redis_root = RedisRoot( 553 | prefix=prefix, 554 | connection_pool=connection_pool, 555 | ignore_deserialization_errors=True 556 | ) 557 | have_exception = True 558 | try: 559 | some_dict = { 560 | 'age': 19, 561 | 'weed': True 562 | } 563 | DictCheckModel( 564 | redis_root=redis_root, 565 | redis_dict=some_dict 566 | ).save() 567 | dict_check_model_instance = redis_root.get(DictCheckModel)[0] 568 | if 'redis_dict' in dict_check_model_instance.keys(): 569 | if dict_check_model_instance['redis_dict'] == some_dict: 570 | have_exception = False 571 | except BaseException as ex: 572 | print(ex) 573 | 574 | clean_db_after_test(connection_pool, prefix) 575 | return have_exception 576 | 577 | 578 | def list_test(connection_pool, prefix): 579 | redis_root = RedisRoot( 580 | prefix=prefix, 581 | connection_pool=connection_pool, 582 | ignore_deserialization_errors=True 583 | ) 584 | have_exception = True 585 | try: 586 | some_list = [5, 9, 's', 4.5, False] 587 | ListCheckModel( 588 | redis_root=redis_root, 589 | redis_list=some_list 590 | ).save() 591 | list_check_model_instance = redis_root.get(ListCheckModel)[0] 592 | if 'redis_list' in list_check_model_instance.keys(): 593 | if list_check_model_instance['redis_list'] == some_list: 594 | have_exception = False 595 | except BaseException as ex: 596 | print(ex) 597 | 598 | clean_db_after_test(connection_pool, prefix) 599 | return have_exception 600 | 601 | 602 | def non_blocking_test(connection_pool, prefix): 603 | have_exception = True 604 | 605 | # try: 606 | 607 | def task(data_count, use_non_blocking): 608 | connection_pool = redis.ConnectionPool( 609 | host=os.environ['REDIS_HOST'], 610 | port=os.environ['REDIS_PORT'], 611 | db=0, 612 | decode_responses=True 613 | ) 614 | redis_root = RedisRoot( 615 | prefix=prefix, 616 | connection_pool=connection_pool, 617 | ignore_deserialization_errors=True 618 | ) 619 | 620 | for i in range(data_count): 621 | redis_root.create( 622 | ListCheckModel, 623 | redis_list=['update_list'] 624 | ) 625 | redis_root.create( 626 | ListCheckModel, 627 | redis_list=['delete_list'] 628 | ) 629 | 630 | def create_list(): 631 | if use_non_blocking: 632 | list_check_model_instance = redis_root.create_nb( 633 | ListCheckModel, 634 | redis_list=['create_list'] 635 | ) 636 | else: 637 | list_check_model_instance = redis_root.create( 638 | ListCheckModel, 639 | redis_list=['create_list'] 640 | ) 641 | 642 | def update_list(): 643 | to_update = redis_root.get( 644 | ListCheckModel, 645 | redis_list=['update_list'] 646 | ) 647 | if use_non_blocking: 648 | updated_instance = redis_root.update_nb( 649 | ListCheckModel, 650 | to_update, 651 | redis_list=['now_updated_list'] 652 | ) 653 | else: 654 | updated_instance = redis_root.update( 655 | ListCheckModel, 656 | to_update, 657 | redis_list=['now_updated_list'] 658 | ) 659 | 660 | def delete_list(): 661 | to_delete = redis_root.get( 662 | ListCheckModel, 663 | redis_list=['delete_list'] 664 | ) 665 | if use_non_blocking: 666 | redis_root.delete_nb( 667 | ListCheckModel, 668 | to_delete, 669 | ) 670 | else: 671 | redis_root.delete( 672 | ListCheckModel, 673 | to_delete, 674 | ) 675 | 676 | tests = [ 677 | create_list, 678 | update_list, 679 | delete_list, 680 | ] 681 | for test in tests: 682 | for i in range(data_count): 683 | test() 684 | 685 | data_count = 100 686 | clean_db_after_test(connection_pool, prefix) 687 | nb_started_in = datetime.datetime.now() 688 | task(data_count, True) 689 | nb_ended_in = datetime.datetime.now() 690 | nb_time = (nb_ended_in - nb_started_in).total_seconds() 691 | clean_db_after_test(connection_pool, prefix) 692 | b_started_in = datetime.datetime.now() 693 | task(data_count, False) 694 | b_ended_in = datetime.datetime.now() 695 | b_time = (b_ended_in - b_started_in).total_seconds() 696 | clean_db_after_test(connection_pool, prefix) 697 | 698 | nb_percent = round((nb_time / b_time - 1) * 100, 2) 699 | nb_symbol = ('+' if nb_percent > 0 else '') 700 | print(f'Non blocking gives {nb_symbol}{nb_percent}% efficiency') 701 | have_exception = False 702 | # except BaseException as ex: 703 | # print(ex) 704 | 705 | clean_db_after_test(connection_pool, prefix) 706 | return have_exception 707 | 708 | 709 | def foreign_key_test(connection_pool, prefix): 710 | redis_root = RedisRoot( 711 | prefix=prefix, 712 | connection_pool=connection_pool, 713 | ignore_deserialization_errors=True, 714 | ) 715 | have_exception = False 716 | try: 717 | task_id = 12345 718 | task_challenge = TaskChallenge( 719 | redis_root=redis_root, 720 | task_id=task_id 721 | ).save() 722 | foreign_key_check_instance = redis_root.create( 723 | ForeignKeyCheckModel, 724 | task_challenge=task_challenge 725 | ) 726 | # Check really created 727 | task_challenge_qs = redis_root.get(TaskChallenge, task_id=task_id) 728 | if len(task_challenge_qs) != 1: 729 | have_exception = True 730 | else: 731 | task_challenge = task_challenge_qs[0] 732 | foreign_key_check_instance_qs = redis_root.get(ForeignKeyCheckModel, task_challenge=task_challenge) 733 | if len(foreign_key_check_instance_qs) != 1: 734 | have_exception = True 735 | else: 736 | foreign_key_check_instance = foreign_key_check_instance_qs[0] 737 | if foreign_key_check_instance['task_challenge']['task_id'] != task_id: 738 | have_exception = True 739 | except BaseException as ex: 740 | print(ex) 741 | have_exception = True 742 | 743 | clean_db_after_test(connection_pool, prefix) 744 | return have_exception 745 | 746 | 747 | def many_to_many_test(connection_pool, prefix): 748 | redis_root = RedisRoot( 749 | prefix=prefix, 750 | connection_pool=connection_pool, 751 | ignore_deserialization_errors=True, 752 | ) 753 | have_exception = False 754 | try: 755 | tasks_ids = set([random.randrange(0, 100) for i in range(10)]) 756 | task_challenges = [ 757 | TaskChallenge( 758 | redis_root=redis_root, 759 | task_id=task_id 760 | ).save() 761 | for task_id in tasks_ids 762 | ] 763 | many_to_many_check_instance = redis_root.create( 764 | ManyToManyCheckModel, 765 | task_challenges=task_challenges 766 | ) 767 | # Check really created 768 | many_to_many_check_instances_qs = redis_root.get(ManyToManyCheckModel) 769 | if len(many_to_many_check_instances_qs) != 1: 770 | have_exception = True 771 | else: 772 | many_to_many_check_instance = many_to_many_check_instances_qs[0] 773 | if many_to_many_check_instance['task_challenges'] != task_challenges: 774 | have_exception = True 775 | except BaseException as ex: 776 | print(ex) 777 | have_exception = True 778 | 779 | clean_db_after_test(connection_pool, prefix) 780 | return have_exception 781 | 782 | 783 | def save_override_test(connection_pool, prefix): 784 | redis_root = RedisRoot( 785 | prefix=prefix, 786 | connection_pool=connection_pool, 787 | ignore_deserialization_errors=True, 788 | ) 789 | have_exception = False 790 | try: 791 | instance_1 = redis_root.create(ModelWithOverriddenSave) 792 | instance_2 = redis_root.create(ModelWithOverriddenSave) 793 | if instance_1['multiplied_max_field'] * 2 != instance_2['multiplied_max_field']: 794 | have_exception = True 795 | except BaseException as ex: 796 | print(ex) 797 | have_exception = True 798 | 799 | clean_db_after_test(connection_pool, prefix) 800 | return have_exception 801 | 802 | 803 | def performance_test(connection_pool, prefix): 804 | have_exception = False 805 | try: 806 | 807 | def run_test(count, model): 808 | 809 | def test(count, model, **test_params): 810 | real_test_params = { 811 | 'use_keys': True, 812 | 'use_non_blocking': True 813 | } 814 | for key in real_test_params.copy(): 815 | if key in test_params.keys(): 816 | real_test_params[key] = test_params[key] 817 | 818 | def create_instances(redis_root, count, use_non_blocking, model): 819 | 820 | if use_non_blocking: 821 | started_in = datetime.datetime.now() 822 | results = [ 823 | redis_root.create_nb(model) 824 | for i in range(count) 825 | ] 826 | ended_in = datetime.datetime.now() 827 | else: 828 | started_in = datetime.datetime.now() 829 | results = [ 830 | redis_root.create(model) 831 | for i in range(count) 832 | ] 833 | ended_in = datetime.datetime.now() 834 | 835 | time_took = (ended_in - started_in).total_seconds() 836 | fields_count = len(results[0].keys()) * count 837 | clean_db_after_test(connection_pool, prefix) 838 | return [time_took, count, fields_count] 839 | 840 | redis_root = RedisRoot( 841 | prefix=prefix, 842 | connection_pool=connection_pool, 843 | ignore_deserialization_errors=True, 844 | use_keys=real_test_params['use_keys'] 845 | ) 846 | 847 | test_result = create_instances(redis_root, count, real_test_params['use_non_blocking'], model) 848 | 849 | return test_result 850 | 851 | test_confs = [ 852 | { 853 | 'use_keys': False, 854 | 'use_non_blocking': False, 855 | }, 856 | { 857 | 'use_keys': False, 858 | 'use_non_blocking': True, 859 | }, 860 | { 861 | 'use_keys': True, 862 | 'use_non_blocking': False, 863 | }, 864 | { 865 | 'use_keys': True, 866 | 'use_non_blocking': True, 867 | }, 868 | 869 | ] 870 | 871 | test_confs_results = [ 872 | test(count, model, **test_conf) 873 | for test_conf in test_confs 874 | ] 875 | 876 | print(f'\n\n\n' 877 | f'Performance test results on your machine:\n' 878 | f'Every test creates {test_confs_results[0][1]} instances ({test_confs_results[0][2]} fields) of {model.__name__} model,\n' 879 | f'Here is the results:\n' 880 | f'\n') 881 | 882 | min_time = min(list(map(lambda result: result[0], test_confs_results))) 883 | min_conf_text = '' 884 | for i, test_confs_result in enumerate(test_confs_results): 885 | test_conf_text = ", ".join([f"{k} = {v}" for k, v in test_confs[i].items()]) 886 | print(f'Configuration: {test_conf_text} took {test_confs_result[0]}s') 887 | if test_confs_result[0] == min_time: 888 | min_conf_text = test_conf_text 889 | print(f'\n\n' 890 | f'The best configuration: {min_conf_text}\n') 891 | 892 | count = 1000 893 | model = TaskChallenge 894 | run_test(count, model) 895 | except BaseException as ex: 896 | print(ex) 897 | have_exception = True 898 | 899 | clean_db_after_test(connection_pool, prefix) 900 | return have_exception 901 | 902 | 903 | def run_tests(): 904 | connection_pool = redis.ConnectionPool( 905 | host=os.environ['REDIS_HOST'], 906 | port=os.environ['REDIS_PORT'], 907 | db=0, 908 | decode_responses=True 909 | ) 910 | tests = [ 911 | basic_test, 912 | auto_reg_test, 913 | no_connection_pool_test, 914 | choices_test, 915 | order_test, 916 | filter_test, 917 | functions_like_defaults_test, 918 | redis_foreign_key_test, 919 | update_test, 920 | delete_test, 921 | save_consistency_test, 922 | meta_ttl_test, 923 | use_keys_test, 924 | list_test, 925 | dict_test, 926 | non_blocking_test, 927 | foreign_key_test, 928 | many_to_many_test, 929 | save_override_test, 930 | performance_test, 931 | ] 932 | results = [] 933 | started_in = datetime.datetime.now() 934 | print('STARTING TESTS\n') 935 | for i, test in enumerate(tests): 936 | print(f'Starting {int(i + 1)} test: {test.__name__.replace("_", " ")}') 937 | test_started_in = datetime.datetime.now() 938 | result = not test(connection_pool, test.__name__) 939 | test_ended_in = datetime.datetime.now() 940 | test_time = (test_ended_in - test_started_in).total_seconds() 941 | print(f'{result = } / {test_time}s\n') 942 | results.append(result) 943 | ended_in = datetime.datetime.now() 944 | time = (ended_in - started_in).total_seconds() 945 | success_message = 'SUCCESS' if all(results) else 'FAILED' 946 | print('\n' 947 | f'{success_message}!\n') 948 | results_success_count = 0 949 | for i, result in enumerate(results): 950 | result_message = 'SUCCESS' if result else 'FAILED' 951 | print(f'Test {(i + 1)}/{len(results)}: {result_message} ({tests[i].__name__.replace("_", " ")})') 952 | if result: 953 | results_success_count += 1 954 | print(f'\n' 955 | f'{results_success_count} / {len(results)} tests ran successfully\n' 956 | f'All tests completed in {time}s\n') 957 | 958 | return all(results) 959 | 960 | 961 | if __name__ == '__main__': 962 | results = run_tests() 963 | if not results: 964 | sys.exit(1) 965 | 966 | ``` 967 | 968 | 969 | ### Output 970 | 971 | ``` 972 | STARTING TESTS 973 | 974 | Starting 1 test: basic test 975 | result = True / 0.017655s 976 | 977 | Starting 2 test: auto reg test 978 | result = True / 0.002688s 979 | 980 | Starting 3 test: no connection pool test 981 | 2021-09-17 13:33:42.915213 - RedisRoot: No connection_pool provided, trying default config... 982 | result = True / 0.003571s 983 | 984 | Starting 4 test: choices test 985 | result = True / 0.001307s 986 | 987 | Starting 5 test: order test 988 | result = True / 0.005999s 989 | 990 | Starting 6 test: filter test 991 | result = True / 0.019395s 992 | 993 | Starting 7 test: functions like defaults test 994 | result = True / 0.003462s 995 | 996 | Starting 8 test: redis foreign key test 997 | result = True / 0.006697s 998 | 999 | Starting 9 test: update test 1000 | result = True / 0.003751s 1001 | 1002 | Starting 10 test: delete test 1003 | result = True / 0.006936s 1004 | 1005 | Starting 11 test: save consistency test 1006 | result = True / 6.009583s 1007 | 1008 | Starting 12 test: meta ttl test 1009 | result = True / 6.010543s 1010 | 1011 | Starting 13 test: use keys test 1012 | Keys usage gives +32.47% efficiency 1013 | result = True / 1.348907s 1014 | 1015 | Starting 14 test: list test 1016 | result = True / 0.002379s 1017 | 1018 | Starting 15 test: dict test 1019 | result = True / 0.002316s 1020 | 1021 | Starting 16 test: non blocking test 1022 | Non blocking gives +195.91% efficiency 1023 | result = True / 13.0517s 1024 | 1025 | Starting 17 test: foreign key test 1026 | result = True / 0.00598s 1027 | 1028 | Starting 18 test: many to many test 1029 | result = True / 0.033524s 1030 | 1031 | Starting 19 test: save override test 1032 | result = True / 0.003881s 1033 | 1034 | Starting 20 test: performance test 1035 | 1036 | 1037 | 1038 | Performance test results on your machine: 1039 | Every test creates 1000 instances (6000 fields) of TaskChallenge model, 1040 | Here is the results: 1041 | 1042 | 1043 | Configuration: use_keys = False, use_non_blocking = False took 40.92255s 1044 | Configuration: use_keys = False, use_non_blocking = True took 0.234719s 1045 | Configuration: use_keys = True, use_non_blocking = False took 31.373307s 1046 | Configuration: use_keys = True, use_non_blocking = True took 0.242484s 1047 | 1048 | 1049 | The best configuration: use_keys = False, use_non_blocking = True 1050 | 1051 | result = True / 73.106303s 1052 | 1053 | 1054 | SUCCESS! 1055 | 1056 | Test 1/20: SUCCESS (basic test) 1057 | Test 2/20: SUCCESS (auto reg test) 1058 | Test 3/20: SUCCESS (no connection pool test) 1059 | Test 4/20: SUCCESS (choices test) 1060 | Test 5/20: SUCCESS (order test) 1061 | Test 6/20: SUCCESS (filter test) 1062 | Test 7/20: SUCCESS (functions like defaults test) 1063 | Test 8/20: SUCCESS (redis foreign key test) 1064 | Test 9/20: SUCCESS (update test) 1065 | Test 10/20: SUCCESS (delete test) 1066 | Test 11/20: SUCCESS (save consistency test) 1067 | Test 12/20: SUCCESS (meta ttl test) 1068 | Test 13/20: SUCCESS (use keys test) 1069 | Test 14/20: SUCCESS (list test) 1070 | Test 15/20: SUCCESS (dict test) 1071 | Test 16/20: SUCCESS (non blocking test) 1072 | Test 17/20: SUCCESS (foreign key test) 1073 | Test 18/20: SUCCESS (many to many test) 1074 | Test 19/20: SUCCESS (save override test) 1075 | Test 20/20: SUCCESS (performance test) 1076 | 1077 | 20 / 20 tests ran successfully 1078 | All tests completed in 99.646971s 1079 | 1080 | ``` -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/__init__.py -------------------------------------------------------------------------------- /dist/python-redis-orm-0.1.0.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python-redis-orm-0.1.0.tar.gz -------------------------------------------------------------------------------- /dist/python-redis-orm-0.1.1.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python-redis-orm-0.1.1.tar.gz -------------------------------------------------------------------------------- /dist/python-redis-orm-0.1.2.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python-redis-orm-0.1.2.tar.gz -------------------------------------------------------------------------------- /dist/python-redis-orm-0.1.3.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python-redis-orm-0.1.3.tar.gz -------------------------------------------------------------------------------- /dist/python-redis-orm-0.1.4.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python-redis-orm-0.1.4.tar.gz -------------------------------------------------------------------------------- /dist/python-redis-orm-0.1.7.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python-redis-orm-0.1.7.tar.gz -------------------------------------------------------------------------------- /dist/python-redis-orm-0.1.8.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python-redis-orm-0.1.8.tar.gz -------------------------------------------------------------------------------- /dist/python-redis-orm-0.1.9.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python-redis-orm-0.1.9.tar.gz -------------------------------------------------------------------------------- /dist/python-redis-orm-0.2.0.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python-redis-orm-0.2.0.tar.gz -------------------------------------------------------------------------------- /dist/python-redis-orm-0.2.1.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python-redis-orm-0.2.1.tar.gz -------------------------------------------------------------------------------- /dist/python-redis-orm-0.2.2.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python-redis-orm-0.2.2.tar.gz -------------------------------------------------------------------------------- /dist/python-redis-orm-0.2.3.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python-redis-orm-0.2.3.tar.gz -------------------------------------------------------------------------------- /dist/python-redis-orm-0.2.4.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python-redis-orm-0.2.4.tar.gz -------------------------------------------------------------------------------- /dist/python-redis-orm-0.2.5.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python-redis-orm-0.2.5.tar.gz -------------------------------------------------------------------------------- /dist/python-redis-orm-0.2.7.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python-redis-orm-0.2.7.tar.gz -------------------------------------------------------------------------------- /dist/python-redis-orm-0.2.8.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python-redis-orm-0.2.8.tar.gz -------------------------------------------------------------------------------- /dist/python-redis-orm-0.2.9.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python-redis-orm-0.2.9.tar.gz -------------------------------------------------------------------------------- /dist/python-redis-orm-0.3.0.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python-redis-orm-0.3.0.tar.gz -------------------------------------------------------------------------------- /dist/python-redis-orm-0.3.1.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python-redis-orm-0.3.1.tar.gz -------------------------------------------------------------------------------- /dist/python-redis-orm-0.3.2.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python-redis-orm-0.3.2.tar.gz -------------------------------------------------------------------------------- /dist/python-redis-orm-0.3.3.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python-redis-orm-0.3.3.tar.gz -------------------------------------------------------------------------------- /dist/python-redis-orm-0.3.4.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python-redis-orm-0.3.4.tar.gz -------------------------------------------------------------------------------- /dist/python-redis-orm-0.3.5.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python-redis-orm-0.3.5.tar.gz -------------------------------------------------------------------------------- /dist/python-redis-orm-0.3.6.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python-redis-orm-0.3.6.tar.gz -------------------------------------------------------------------------------- /dist/python-redis-orm-0.3.7.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python-redis-orm-0.3.7.tar.gz -------------------------------------------------------------------------------- /dist/python-redis-orm-0.3.8.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python-redis-orm-0.3.8.tar.gz -------------------------------------------------------------------------------- /dist/python-redis-orm-0.3.9.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python-redis-orm-0.3.9.tar.gz -------------------------------------------------------------------------------- /dist/python-redis-orm-0.4.0.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python-redis-orm-0.4.0.tar.gz -------------------------------------------------------------------------------- /dist/python-redis-orm-0.4.1.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python-redis-orm-0.4.1.tar.gz -------------------------------------------------------------------------------- /dist/python-redis-orm-0.4.2.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python-redis-orm-0.4.2.tar.gz -------------------------------------------------------------------------------- /dist/python-redis-orm-0.4.3.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python-redis-orm-0.4.3.tar.gz -------------------------------------------------------------------------------- /dist/python-redis-orm-0.4.4.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python-redis-orm-0.4.4.tar.gz -------------------------------------------------------------------------------- /dist/python-redis-orm-0.4.5.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python-redis-orm-0.4.5.tar.gz -------------------------------------------------------------------------------- /dist/python-redis-orm-0.4.6.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python-redis-orm-0.4.6.tar.gz -------------------------------------------------------------------------------- /dist/python-redis-orm-0.4.7.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python-redis-orm-0.4.7.tar.gz -------------------------------------------------------------------------------- /dist/python-redis-orm-0.4.8.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python-redis-orm-0.4.8.tar.gz -------------------------------------------------------------------------------- /dist/python-redis-orm-0.4.9.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python-redis-orm-0.4.9.tar.gz -------------------------------------------------------------------------------- /dist/python-redis-orm-0.5.0.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python-redis-orm-0.5.0.tar.gz -------------------------------------------------------------------------------- /dist/python-redis-orm-0.5.1.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python-redis-orm-0.5.1.tar.gz -------------------------------------------------------------------------------- /dist/python-redis-orm-0.5.2.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python-redis-orm-0.5.2.tar.gz -------------------------------------------------------------------------------- /dist/python-redis-orm-0.5.3.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python-redis-orm-0.5.3.tar.gz -------------------------------------------------------------------------------- /dist/python-redis-orm-0.5.4.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python-redis-orm-0.5.4.tar.gz -------------------------------------------------------------------------------- /dist/python_redis_orm-0.1.0-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python_redis_orm-0.1.0-py3-none-any.whl -------------------------------------------------------------------------------- /dist/python_redis_orm-0.1.1-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python_redis_orm-0.1.1-py3-none-any.whl -------------------------------------------------------------------------------- /dist/python_redis_orm-0.1.2-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python_redis_orm-0.1.2-py3-none-any.whl -------------------------------------------------------------------------------- /dist/python_redis_orm-0.1.3-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python_redis_orm-0.1.3-py3-none-any.whl -------------------------------------------------------------------------------- /dist/python_redis_orm-0.1.4-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python_redis_orm-0.1.4-py3-none-any.whl -------------------------------------------------------------------------------- /dist/python_redis_orm-0.1.4.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python_redis_orm-0.1.4.tar.gz -------------------------------------------------------------------------------- /dist/python_redis_orm-0.1.5-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python_redis_orm-0.1.5-py3-none-any.whl -------------------------------------------------------------------------------- /dist/python_redis_orm-0.1.5.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python_redis_orm-0.1.5.tar.gz -------------------------------------------------------------------------------- /dist/python_redis_orm-0.1.6-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python_redis_orm-0.1.6-py3-none-any.whl -------------------------------------------------------------------------------- /dist/python_redis_orm-0.1.6.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python_redis_orm-0.1.6.tar.gz -------------------------------------------------------------------------------- /dist/python_redis_orm-0.1.7-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python_redis_orm-0.1.7-py3-none-any.whl -------------------------------------------------------------------------------- /dist/python_redis_orm-0.1.8-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python_redis_orm-0.1.8-py3-none-any.whl -------------------------------------------------------------------------------- /dist/python_redis_orm-0.1.9-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python_redis_orm-0.1.9-py3-none-any.whl -------------------------------------------------------------------------------- /dist/python_redis_orm-0.2.0-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python_redis_orm-0.2.0-py3-none-any.whl -------------------------------------------------------------------------------- /dist/python_redis_orm-0.2.1-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python_redis_orm-0.2.1-py3-none-any.whl -------------------------------------------------------------------------------- /dist/python_redis_orm-0.2.2-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python_redis_orm-0.2.2-py3-none-any.whl -------------------------------------------------------------------------------- /dist/python_redis_orm-0.2.3-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python_redis_orm-0.2.3-py3-none-any.whl -------------------------------------------------------------------------------- /dist/python_redis_orm-0.2.4-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python_redis_orm-0.2.4-py3-none-any.whl -------------------------------------------------------------------------------- /dist/python_redis_orm-0.2.5-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python_redis_orm-0.2.5-py3-none-any.whl -------------------------------------------------------------------------------- /dist/python_redis_orm-0.2.7-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python_redis_orm-0.2.7-py3-none-any.whl -------------------------------------------------------------------------------- /dist/python_redis_orm-0.2.8-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python_redis_orm-0.2.8-py3-none-any.whl -------------------------------------------------------------------------------- /dist/python_redis_orm-0.2.9-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python_redis_orm-0.2.9-py3-none-any.whl -------------------------------------------------------------------------------- /dist/python_redis_orm-0.3.0-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python_redis_orm-0.3.0-py3-none-any.whl -------------------------------------------------------------------------------- /dist/python_redis_orm-0.3.1-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python_redis_orm-0.3.1-py3-none-any.whl -------------------------------------------------------------------------------- /dist/python_redis_orm-0.3.2-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python_redis_orm-0.3.2-py3-none-any.whl -------------------------------------------------------------------------------- /dist/python_redis_orm-0.3.3-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python_redis_orm-0.3.3-py3-none-any.whl -------------------------------------------------------------------------------- /dist/python_redis_orm-0.3.4-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python_redis_orm-0.3.4-py3-none-any.whl -------------------------------------------------------------------------------- /dist/python_redis_orm-0.3.5-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python_redis_orm-0.3.5-py3-none-any.whl -------------------------------------------------------------------------------- /dist/python_redis_orm-0.3.6-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python_redis_orm-0.3.6-py3-none-any.whl -------------------------------------------------------------------------------- /dist/python_redis_orm-0.3.7-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python_redis_orm-0.3.7-py3-none-any.whl -------------------------------------------------------------------------------- /dist/python_redis_orm-0.3.8-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python_redis_orm-0.3.8-py3-none-any.whl -------------------------------------------------------------------------------- /dist/python_redis_orm-0.3.9-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python_redis_orm-0.3.9-py3-none-any.whl -------------------------------------------------------------------------------- /dist/python_redis_orm-0.4.0-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python_redis_orm-0.4.0-py3-none-any.whl -------------------------------------------------------------------------------- /dist/python_redis_orm-0.4.1-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python_redis_orm-0.4.1-py3-none-any.whl -------------------------------------------------------------------------------- /dist/python_redis_orm-0.4.2-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python_redis_orm-0.4.2-py3-none-any.whl -------------------------------------------------------------------------------- /dist/python_redis_orm-0.4.3-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python_redis_orm-0.4.3-py3-none-any.whl -------------------------------------------------------------------------------- /dist/python_redis_orm-0.4.4-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python_redis_orm-0.4.4-py3-none-any.whl -------------------------------------------------------------------------------- /dist/python_redis_orm-0.4.5-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python_redis_orm-0.4.5-py3-none-any.whl -------------------------------------------------------------------------------- /dist/python_redis_orm-0.4.6-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python_redis_orm-0.4.6-py3-none-any.whl -------------------------------------------------------------------------------- /dist/python_redis_orm-0.4.7-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python_redis_orm-0.4.7-py3-none-any.whl -------------------------------------------------------------------------------- /dist/python_redis_orm-0.4.8-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python_redis_orm-0.4.8-py3-none-any.whl -------------------------------------------------------------------------------- /dist/python_redis_orm-0.4.9-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python_redis_orm-0.4.9-py3-none-any.whl -------------------------------------------------------------------------------- /dist/python_redis_orm-0.5.0-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python_redis_orm-0.5.0-py3-none-any.whl -------------------------------------------------------------------------------- /dist/python_redis_orm-0.5.1-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python_redis_orm-0.5.1-py3-none-any.whl -------------------------------------------------------------------------------- /dist/python_redis_orm-0.5.2-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python_redis_orm-0.5.2-py3-none-any.whl -------------------------------------------------------------------------------- /dist/python_redis_orm-0.5.3-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python_redis_orm-0.5.3-py3-none-any.whl -------------------------------------------------------------------------------- /dist/python_redis_orm-0.5.4-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/dist/python_redis_orm-0.5.4-py3-none-any.whl -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "python-redis-orm" 3 | version = "0.5.4" 4 | description = "Python Redis ORM library that gives redis easy-to-use objects with fields and speeds a development up, inspired by Django ORM" 5 | authors = ["Anton Nechaev "] 6 | readme = "README.md" 7 | repository = "https://github.com/gh0st-work/python_redis_orm" 8 | homepage = "https://github.com/gh0st-work/python_redis_orm" 9 | keywords = ["python", "redis", "ORM", "django", "database"] 10 | license = "MIT" 11 | classifiers = [ 12 | "Development Status :: 5 - Production/Stable", 13 | "Intended Audience :: Developers", 14 | "Environment :: Web Environment", 15 | "Operating System :: OS Independent", 16 | "Topic :: Database :: Front-Ends", 17 | "Topic :: Internet :: WWW/HTTP :: Dynamic Content", 18 | "Topic :: Software Development", 19 | "Topic :: Software Development :: Libraries", 20 | "Topic :: Software Development :: Libraries :: Python Modules", 21 | "Topic :: Software Development :: Object Brokering", 22 | "Topic :: Utilities", 23 | "License :: OSI Approved :: MIT License", 24 | "Framework :: Django", 25 | "Framework :: Django :: 3.0", 26 | "Programming Language :: Python", 27 | "Programming Language :: Python :: 3", 28 | "Programming Language :: Python :: 3 :: Only", 29 | "Programming Language :: Python :: 3.6", 30 | "Programming Language :: Python :: 3.7", 31 | "Programming Language :: Python :: 3.8", 32 | ] 33 | 34 | [tool.poetry.dependencies] 35 | python = "^3.6" 36 | redis = "^3.5.3" 37 | pytz = "^2021" 38 | 39 | [tool.poetry.dev-dependencies] 40 | 41 | [build-system] 42 | requires = ["poetry-core>=1.0.0"] 43 | build-backend = "poetry.core.masonry.api" 44 | -------------------------------------------------------------------------------- /python_redis_orm/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gh0st-work/python_redis_orm/91833206cd7fb23e665aadf40ebece5d8747c5bd/python_redis_orm/__init__.py -------------------------------------------------------------------------------- /python_redis_orm/core.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import datetime 3 | import decimal 4 | import json 5 | from copy import deepcopy 6 | from functools import reduce 7 | 8 | import pytz 9 | import redis 10 | 11 | from python_redis_orm.utils import check_types, get_ids_from_untyped_data, check_callable 12 | 13 | 14 | ### FIELDS ### 15 | 16 | 17 | class RedisField: 18 | 19 | def __init__(self, default=None, choices=None, null=True): 20 | default = check_callable(default) 21 | choices = check_callable(choices) 22 | null = check_callable(null) 23 | check_types(choices, dict) 24 | check_types(null, bool) 25 | self.default = default 26 | self.value = None 27 | self.choices = choices 28 | self.null = null 29 | 30 | def _get_default_value(self): 31 | self.value = check_callable(self.default) 32 | return self.value 33 | 34 | def _check_choices(self, value): 35 | if self.choices: 36 | if value not in self.choices.keys(): 37 | raise Exception(f'{value} is not allowed. Allowed values: {", ".join(list(self.choices.keys()))}') 38 | 39 | def check_value(self): 40 | if self.value is None: 41 | self.value = self._get_default_value() 42 | if self.value is None: 43 | if self.null: 44 | self.value = 'null' 45 | else: 46 | raise Exception('null is not allowed') 47 | if self.value: 48 | self._check_choices(self.value) 49 | return self.value 50 | 51 | def clean(self): 52 | self.value = self.check_value() 53 | return self.value 54 | 55 | def deserialize_value_check_null(self, value, redis_root): 56 | if value == 'null': 57 | if not self.null: 58 | if redis_root.ignore_deserialization_errors: 59 | print( 60 | f'{datetime.datetime.now()} - {value} can not be deserialized like {self.__class__.__name__}, ignoring') 61 | else: 62 | raise Exception(f'{value} can not be deserialized like {self.__class__.__name__}') 63 | 64 | def deserialize_value(self, value, redis_root): 65 | self.deserialize_value_check_null(value, redis_root) 66 | return value 67 | 68 | 69 | class RedisString(RedisField): 70 | 71 | def clean(self): 72 | self.value = self.check_value() 73 | if self.value not in [None, 'null']: 74 | self.value = f'{self.value}' 75 | return super().clean() 76 | 77 | def deserialize_value(self, value, redis_root): 78 | self.deserialize_value_check_null(value, redis_root) 79 | value = super().deserialize_value(value, redis_root) 80 | if value not in ['null', None]: 81 | value = f'{value}' 82 | else: 83 | value = None 84 | return value 85 | 86 | 87 | class RedisNumber(RedisField): 88 | 89 | def clean(self): 90 | self.value = self.check_value() 91 | if self.value not in [None, 'null']: 92 | check_types(self.value, (int, float)) 93 | return super().clean() 94 | 95 | def deserialize_value(self, value, redis_root): 96 | self.deserialize_value_check_null(value, redis_root) 97 | if value not in ['null', None]: 98 | value = super().deserialize_value(value, redis_root) 99 | if type(value) == str: 100 | if '.' in value: 101 | value = float(value) 102 | else: 103 | value = int(value) 104 | else: 105 | check_types(value, (int, float)) 106 | else: 107 | value = None 108 | return value 109 | 110 | 111 | class RedisId(RedisNumber): 112 | 113 | def __init__(self, *args, **kwargs): 114 | kwargs['null'] = False 115 | super().__init__(*args, **kwargs) 116 | 117 | 118 | class RedisBool(RedisNumber): 119 | 120 | def __init__(self, *args, **kwargs): 121 | kwargs['choices'] = {True: 'Yes', False: 'No'} 122 | super().__init__(*args, **kwargs) 123 | 124 | def clean(self): 125 | self.value = self.check_value() 126 | if self.value not in [None, 'null']: 127 | check_types(self.value, bool) 128 | self.value = int(self.value) 129 | return super().clean() 130 | 131 | def deserialize_value(self, value, redis_root): 132 | self.deserialize_value_check_null(value, redis_root) 133 | if value not in ['null', None]: 134 | value = super().deserialize_value(value, redis_root) 135 | check_types(value, int) 136 | value = bool(value) 137 | return value 138 | 139 | 140 | class RedisDecimal(RedisString): 141 | 142 | def clean(self): 143 | self.value = self.check_value() 144 | if self.value not in [None, 'null']: 145 | check_types(self.value, (int, float, decimal.Decimal)) 146 | return super().clean() 147 | 148 | def deserialize_value(self, value, redis_root): 149 | self.deserialize_value_check_null(value, redis_root) 150 | if value not in ['null', None]: 151 | value = super().deserialize_value(value, redis_root) 152 | value = decimal.Decimal(value) 153 | else: 154 | value = None 155 | return value 156 | 157 | 158 | class RedisJson(RedisField): 159 | 160 | def __init__(self, json_allowed_types=(list, dict), *args, **kwargs): 161 | self.json_allowed_types = json_allowed_types 162 | super().__init__(*args, **kwargs) 163 | 164 | def set_json_allowed_types(self, allowed_types): 165 | self.json_allowed_types = allowed_types 166 | return self.json_allowed_types 167 | 168 | def clean(self): 169 | self.value = self.check_value() 170 | if self.value not in [None, 'null']: 171 | check_types(self.value, self.json_allowed_types) 172 | json_string = json.dumps(self.value) 173 | self.value = json_string 174 | return super().clean() 175 | 176 | def deserialize_value(self, value, redis_root): 177 | self.deserialize_value_check_null(value, redis_root) 178 | if value not in ['null', None]: 179 | value = super().deserialize_value(value, redis_root) 180 | check_types(value, str) 181 | value = json.loads(value) 182 | check_types(value, self.json_allowed_types) 183 | else: 184 | value = None 185 | return value 186 | 187 | 188 | class RedisDict(RedisJson): 189 | 190 | def clean(self): 191 | self.set_json_allowed_types(dict) 192 | return super().clean() 193 | 194 | def deserialize_value(self, value, redis_root): 195 | self.set_json_allowed_types(dict) 196 | self.deserialize_value_check_null(value, redis_root) 197 | if value not in ['null', None]: 198 | value = super().deserialize_value(value, redis_root) 199 | else: 200 | value = None 201 | return value 202 | 203 | 204 | class RedisList(RedisJson): 205 | 206 | def clean(self): 207 | self.set_json_allowed_types(list) 208 | return super().clean() 209 | 210 | def deserialize_value(self, value, redis_root): 211 | self.set_json_allowed_types(list) 212 | self.deserialize_value_check_null(value, redis_root) 213 | if value not in ['null', None]: 214 | value = super().deserialize_value(value, redis_root) 215 | else: 216 | value = None 217 | return value 218 | 219 | 220 | class RedisDateTime(RedisString): 221 | 222 | def clean(self): 223 | self.value = self.check_value() 224 | if self.value not in [None, 'null']: 225 | check_types(self.value, datetime.datetime) 226 | string_datetime = self.value.replace(tzinfo=pytz.UTC).strftime('%Y.%m.%d-%H:%M:%S+%Z') 227 | self.value = string_datetime 228 | return super().clean() 229 | 230 | def deserialize_value(self, value, redis_root): 231 | self.deserialize_value_check_null(value, redis_root) 232 | if value not in ['null', None]: 233 | value = super().deserialize_value(value, redis_root) 234 | check_types(value, str) 235 | value = datetime.datetime.strptime(value, '%Y.%m.%d-%H:%M:%S+%Z').replace(tzinfo=pytz.UTC) 236 | else: 237 | value = None 238 | return value 239 | 240 | 241 | class RedisDate(RedisString): 242 | 243 | def clean(self): 244 | self.value = self.check_value() 245 | if self.value not in [None, 'null']: 246 | check_types(self.value, datetime.date) 247 | string_date = self.value.strftime('%Y.%m.%d+%Z') 248 | self.value = string_date 249 | return super().clean() 250 | 251 | def deserialize_value(self, value, redis_root): 252 | self.deserialize_value_check_null(value, redis_root) 253 | if value not in ['null', None]: 254 | value = super().deserialize_value(value, redis_root) 255 | check_types(value, str) 256 | value = datetime.datetime.strptime(value, '%Y.%m.%d+%Z').date() 257 | else: 258 | value = None 259 | return value 260 | 261 | 262 | class RedisForeignKey(RedisNumber): 263 | 264 | def __init__(self, model=None, *args, **kwargs): 265 | model = check_callable(model) 266 | if args: 267 | args = list(map(check_callable, *args)) 268 | allowed = True 269 | if model is None: 270 | allowed = False 271 | elif not issubclass(model, RedisModel): 272 | allowed = False 273 | if not allowed: 274 | raise Exception(f'{model.__name__} class is not RedisModel') 275 | else: 276 | self.model = model 277 | super().__init__(*args, **kwargs) 278 | 279 | def _get_id_from_instance_dict(self): 280 | if self.value: 281 | if type(self.value) == dict: 282 | if 'id' in self.value.keys(): 283 | self.value = self.value['id'] 284 | else: 285 | raise Exception( 286 | f"{self.value} has no key 'id', please provide serialized instance or dict like " + "{'id': 1, ...}") 287 | else: 288 | raise Exception( 289 | f'{self.value} type is not dict, please provide serialized instance or dict like ' + "{'id': 1, ...}") 290 | return self.value 291 | 292 | def clean(self): 293 | self.value = self.check_value() 294 | if self.value not in [None, 'null']: 295 | self.value = get_ids_from_untyped_data(self.value)[0] 296 | return super().clean() 297 | 298 | def deserialize_value(self, value, redis_root): 299 | self.deserialize_value_check_null(value, redis_root) 300 | if value not in ['null', None]: 301 | value = super().deserialize_value(value, redis_root) 302 | check_types(value, int) 303 | instance_id = value 304 | instance_qs = redis_root.get(self.model, return_dict=True, id=instance_id) 305 | if instance_id in instance_qs.keys(): 306 | value = instance_qs[instance_id] 307 | else: 308 | value = {'id': value} 309 | else: 310 | value = None 311 | return value 312 | 313 | 314 | class RedisManyToMany(RedisList): 315 | 316 | def __init__(self, model=None, *args, **kwargs): 317 | model = check_callable(model) 318 | if args: 319 | args = list(map(check_callable, *args)) 320 | allowed = True 321 | if model is None: 322 | allowed = False 323 | elif not issubclass(model, RedisModel): 324 | allowed = False 325 | if not allowed: 326 | raise Exception(f'{model.__name__} class is not RedisModel') 327 | else: 328 | self.model = model 329 | super().__init__(*args, **kwargs) 330 | 331 | def clean(self): 332 | self.value = self.check_value() 333 | if self.value not in [None, 'null']: 334 | self.value = get_ids_from_untyped_data(self.value) 335 | return super().clean() 336 | 337 | def deserialize_value(self, value, redis_root): 338 | self.deserialize_value_check_null(value, redis_root) 339 | if value not in ['null', None]: 340 | value = super().deserialize_value(value, redis_root) 341 | instances_ids = value 342 | instances = redis_root.get(self.model, id__in=instances_ids) 343 | value = instances 344 | 345 | return value 346 | 347 | 348 | ### REDIS ROOT ### 349 | 350 | 351 | class RedisRoot: 352 | 353 | ### INIT ### 354 | 355 | def __init__( 356 | self, 357 | connection_pool=None, 358 | prefix='redis_test', 359 | ignore_deserialization_errors=True, 360 | save_consistency=False, 361 | use_keys=True, 362 | solo_usage=True, 363 | save_type='instances' 364 | ): 365 | connection_pool = check_callable(connection_pool) 366 | prefix = check_callable(prefix) 367 | ignore_deserialization_errors = check_callable(ignore_deserialization_errors) 368 | save_consistency = check_callable(save_consistency) 369 | use_keys = check_callable(use_keys) 370 | check_types(ignore_deserialization_errors, bool) 371 | check_types(save_consistency, bool) 372 | check_types(use_keys, bool) 373 | check_types(solo_usage, bool) 374 | allowed_save_types = ['fields', 'instances'] 375 | if save_type not in allowed_save_types: 376 | raise Exception(f'Save type {save_type} is not allowed. Allowed only: {", ".join(allowed_save_types)}') 377 | self.registered_models = [] 378 | self.registered_django_models = {} 379 | prefix = check_callable(prefix) 380 | if type(prefix) == str: 381 | self.prefix = prefix 382 | else: 383 | print( 384 | f'{datetime.datetime.now()} - Prefix {prefix} is type of {type(prefix)}, allowed only str, using default prefix "redis_test"') 385 | self.prefix = 'redis_test' 386 | self.connection_pool = self._get_connection_pool(connection_pool) 387 | self.ignore_deserialization_errors = ignore_deserialization_errors 388 | self.save_consistency = save_consistency 389 | self.use_keys = use_keys 390 | self.creating = {} 391 | self.solo_usage = solo_usage 392 | self.max_models_ids = {} 393 | self.save_type = save_type 394 | self.set_wait_creating(False) 395 | 396 | @property 397 | def redis_instance(self): 398 | redis_instance = redis.Redis(connection_pool=self.connection_pool) 399 | redis_instance = redis.Redis(connection_pool=self.connection_pool) 400 | return redis_instance 401 | 402 | def _get_connection_pool(self, connection_pool): 403 | if isinstance(connection_pool, redis.ConnectionPool): 404 | connection_pool.connection_kwargs['decode_responses'] = True 405 | self.connection_pool = connection_pool 406 | else: 407 | print( 408 | f'{datetime.datetime.now()} - {self.__class__.__name__}: No connection_pool provided, trying default config...') 409 | default_host = 'localhost' 410 | default_port = 6379 411 | default_db = 0 412 | try: 413 | connection_pool = redis.ConnectionPool( 414 | decode_responses=True, 415 | host=default_host, 416 | port=default_port, 417 | db=default_db, 418 | ) 419 | self.connection_pool = connection_pool 420 | except BaseException as ex: 421 | raise Exception( 422 | f'Default config ({default_host}:{default_port}, db={default_db}) failed, please provide connection_pool to {self.__class__.__name__}') 423 | return self.connection_pool 424 | 425 | ### UTILS ### 426 | 427 | def register_models(self, models_list): 428 | for model in models_list: 429 | if issubclass(model, RedisModel): 430 | if model not in self.registered_models: 431 | self.registered_models.append(model) 432 | else: 433 | raise Exception(f'{model.__name__} class is not RedisModel') 434 | 435 | def order(self, instances, field_name): 436 | reverse = False 437 | if field_name.startswith('-'): 438 | reverse = True 439 | field_name = field_name[1:] 440 | 441 | return sorted(instances, key=(lambda instance: instance[field_name]), reverse=reverse) 442 | 443 | def get_wait_creating(self): 444 | if self.solo_usage: 445 | return self.is_creating 446 | else: 447 | return bool(int(self.redis_instance.get(f'__creating__:{self.prefix}'))) 448 | 449 | def set_wait_creating(self, is_creating): 450 | if self.solo_usage: 451 | self.is_creating = is_creating 452 | else: 453 | self.redis_instance.set(f'__creating__:{self.prefix}', int(is_creating)) 454 | 455 | def wait_creation(self): 456 | while self.get_wait_creating(): 457 | pass 458 | self.set_wait_creating(True) 459 | 460 | def get_and_reserve_new_id(self, model): 461 | 462 | def really_new(): 463 | new_id = self._get_model_new_id(model) 464 | self.creating[model] = [new_id] 465 | return new_id 466 | 467 | self.wait_creation() 468 | if model not in self.creating.keys(): 469 | new_id = really_new() 470 | elif not len(self.creating[model]): 471 | new_id = really_new() 472 | else: 473 | new_id = self.creating[model][-1] + 1 474 | self.set_wait_creating(False) 475 | return new_id 476 | 477 | def remove_creating(self, model, instance_id): 478 | self.wait_creation() 479 | self.creating[model] = list( 480 | filter(lambda some_instance_id: some_instance_id != instance_id, self.creating[model].copy())) 481 | self.set_wait_creating(False) 482 | 483 | ### GET ### 484 | 485 | def get(self, model, return_dict=False, **filters): 486 | instances = self._get_model_instances(model, filters) 487 | result = self._return_with_format(instances, return_dict) 488 | return result 489 | 490 | def _get_model_instances(self, model, filters): 491 | instances = {} 492 | if self.save_type == 'fields': 493 | instances = self._get_stored_type_fields_model_instances(model, filters) 494 | elif self.save_type == 'instances': 495 | instances = self._get_stored_type_instances_model_instances(model, filters) 496 | if self.save_consistency: 497 | instances = self._check_fields_existence(model, instances) 498 | return instances 499 | 500 | def _get_stored_type_fields_model_instances(self, model, filters): 501 | if not filters: 502 | instances = self._get_instances_data_by_ids(model) 503 | else: 504 | # print() 505 | # print(filters) 506 | cleaned_filters = self._clean_filters(model, filters) 507 | # print(cleaned_filters) 508 | cleaned_filters_with_filtered_ids = self._get_cleaned_filters_with_filtered_ids(cleaned_filters) 509 | # print(cleaned_filters_with_filtered_ids) 510 | starting_model_filtered_ids = self._get_starting_model_filtered_ids(cleaned_filters_with_filtered_ids) 511 | # print(starting_model_filtered_ids) 512 | instances = self._get_instances_data_by_ids(model, starting_model_filtered_ids) 513 | return instances 514 | 515 | def _get_instances_data_by_ids(self, model, ids=None): 516 | model_name = model.__name__ 517 | instances_data = {} 518 | if ids is None: 519 | raw_instances_data = self.fast_get_keys_values(f'{self.prefix}:{model_name}:*') 520 | for raw_instance_key, raw_instance_value in raw_instances_data.items(): 521 | instance_id = int(raw_instance_key.split(':')[-2]) 522 | instance_field_name = raw_instance_key.split(':')[-1] 523 | if instance_id not in instances_data.keys(): 524 | instances_data[instance_id] = {} 525 | instances_data[instance_id][instance_field_name] = raw_instance_value 526 | else: 527 | for instance_id in ids: 528 | raw_fields_data = self.fast_get_keys_values(f'{self.prefix}:{model_name}:{instance_id}:*') 529 | for field_key, field_value in raw_fields_data.items(): 530 | if instance_id not in instances_data.keys(): 531 | instances_data[instance_id] = {} 532 | instances_data[instance_id][field_key.split(':')[-1]] = field_value 533 | instances_data = { 534 | instance_id: { 535 | field_name: self._deserialize_instance_field(model, field_name, field_value) 536 | for field_name, field_value in raw_instance_fields.items() 537 | } 538 | for instance_id, raw_instance_fields in instances_data.copy().items() 539 | } 540 | return instances_data 541 | 542 | def _get_model_new_id(self, model): 543 | if self.solo_usage: 544 | if model not in self.max_models_ids.keys(): 545 | self.max_models_ids[model] = 0 546 | max_id = self.max_models_ids[model] 547 | new_id = max_id + 1 548 | self.max_models_ids[model] = new_id 549 | else: 550 | stored_max_id = self.redis_instance.get(f'max_id:{self.prefix}:{model.__name__}') 551 | max_id = 0 552 | if stored_max_id: 553 | max_id = int(stored_max_id) 554 | new_id = max_id + 1 555 | self.redis_instance.set(f'max_id:{self.prefix}:{model.__name__}', new_id) 556 | return new_id 557 | 558 | def _get_stored_type_instances_model_instances(self, model, filters): 559 | instances_with_allowed = self._get_stored_json_instance_with_allowed(model, filters) 560 | instances = self._get_instances_from_instances_with_allowed(instances_with_allowed) 561 | return instances 562 | 563 | def _get_stored_json_instance_with_allowed(self, model, filters): 564 | model_name = model.__name__ 565 | instances = self._get_instances_by_key(f'{self.prefix}:{model_name}:*') 566 | instances_with_allowed = {} 567 | for instance_id, instance_fields in instances.items(): 568 | for field_name, field_value in instance_fields.items(): 569 | allowed = self._filter_field_name(field_name, field_value, filters) 570 | if instance_id not in instances_with_allowed.keys(): 571 | instances_with_allowed[instance_id] = {} 572 | instances_with_allowed[instance_id][field_name] = { 573 | 'value': field_value, 574 | 'allowed': allowed 575 | } 576 | return instances_with_allowed 577 | 578 | def _get_instances_from_instances_with_allowed(self, instances_with_allowed): 579 | instances = {} 580 | for instance_id, instance_fields in instances_with_allowed.items(): 581 | instance_allowed = all([ 582 | instance_field_data['allowed'] 583 | for instance_field_data in instance_fields.values() 584 | ]) 585 | if instance_allowed and instance_id not in instances.keys(): 586 | instances[instance_id] = { 587 | instance_field_name: instance_field_data['value'] 588 | for instance_field_name, instance_field_data in instance_fields.items() 589 | } 590 | return instances 591 | 592 | def _get_instances_by_key(self, key): 593 | raw_instances = self.fast_get_keys_values(key) 594 | instances = {} 595 | for instance_key, fields_json in raw_instances.items(): 596 | prefix, model_name, instance_id = instance_key.split(':') 597 | model = self._get_registered_model_by_name(model_name) 598 | instance_id = int(instance_id) 599 | fields_dict = json.loads(fields_json) 600 | for field_name, raw_value in fields_dict.items(): 601 | value = self._deserialize_instance_field(model, field_name, raw_value) 602 | if instance_id not in instances.keys(): 603 | instances[instance_id] = {} 604 | instances[instance_id][field_name] = value 605 | return instances 606 | 607 | def _check_fields_existence(self, model, instances): 608 | checked_instances = {} 609 | fields = model.get_class_fields() 610 | if 'id' not in fields.keys(): 611 | fields['id'] = RedisString(null=True) 612 | for instance_id, instance_fields in instances.items(): 613 | checked_instances[instance_id] = {} 614 | for field_name, field in fields.items(): 615 | if field_name in instance_fields.keys(): 616 | checked_instances[instance_id][field_name] = instance_fields[field_name] 617 | else: 618 | checked_instances[instance_id][field_name] = None 619 | return checked_instances 620 | 621 | ### COUNT ### 622 | 623 | def count(self, model, **filters): 624 | count = 0 625 | if not filters: 626 | raw_instances_data = [] 627 | if self.save_type == 'fields': 628 | raw_instances_data = self.fast_get_keys(f'{self.prefix}:{model.__name__}:*:id') 629 | elif self.save_type == 'instances': 630 | raw_instances_data = self.fast_get_keys(f'{self.prefix}:{model.__name__}:*') 631 | count = len(raw_instances_data) 632 | else: 633 | if self.save_type == 'fields': 634 | cleaned_filters = self._clean_filters(model, filters) 635 | cleaned_filters_with_filtered_ids = self._get_cleaned_filters_with_filtered_ids(cleaned_filters) 636 | starting_model_filtered_ids = self._get_starting_model_filtered_ids(cleaned_filters_with_filtered_ids) 637 | count = len(starting_model_filtered_ids) 638 | elif self.save_type == 'instances': 639 | model_name = model.__name__ 640 | instances = self._get_instances_by_key(f'{self.prefix}:{model_name}:*') 641 | instances_with_allowed = {} 642 | for instance_id, instance_fields in instances.items(): 643 | all_fields_allowed = [True, *[ 644 | self._filter_field_name(field_name, field_value, filters) 645 | for field_name, field_value in instance_fields.items() 646 | ]] 647 | if all(all_fields_allowed): 648 | count += 1 649 | return count 650 | 651 | ### DESERIALIZE ### 652 | 653 | def _deserialize_instance_field(self, model, field_name, raw_value): 654 | value = raw_value 655 | saved_field_instance = self._get_field_instance_by_name(model, field_name) 656 | if issubclass(saved_field_instance.__class__, RedisField): 657 | value = self._deserialize_value_by_field_instance(saved_field_instance, raw_value) 658 | return value 659 | 660 | def _get_registered_model_by_name(self, model_name): 661 | found = False 662 | model = None 663 | for registered_model in self.registered_models: 664 | if registered_model.__name__ == model_name and not found: 665 | found = True 666 | model = registered_model 667 | if not found: 668 | if self.ignore_deserialization_errors: 669 | print(f'{datetime.datetime.now()} - {model_name} not found in registered models, ignoring') 670 | model = model_name 671 | else: 672 | raise Exception(f'{model_name} not found in registered models') 673 | return model 674 | 675 | def _get_field_instance_by_name(self, model, field_name): 676 | try: 677 | field_instance = getattr(model, field_name) 678 | except BaseException as ex: 679 | if self.ignore_deserialization_errors: 680 | print( 681 | f'{datetime.datetime.now()} - {model.__name__} has no field {field_name}, ignoring deserialization') 682 | field_instance = field_name 683 | else: 684 | raise Exception(f'{model.__name__} has no field {field_name}') 685 | return field_instance 686 | 687 | def _deserialize_value_by_field_instance(self, field_instance, raw_value): 688 | try: 689 | value = field_instance.deserialize_value(raw_value, self) 690 | except BaseException as ex: 691 | if self.ignore_deserialization_errors: 692 | print( 693 | f'{datetime.datetime.now()} - {raw_value} can not be deserialized like {field_instance.__class__.__name__}, ignoring') 694 | value = raw_value 695 | else: 696 | raise Exception(f'{raw_value} can not be deserialized like {field_instance.__class__.__name__}') 697 | return value 698 | 699 | ### FILTER ### 700 | 701 | def _clean_filters(self, starting_model, filters): 702 | cleaned_filters = {} 703 | for filter_param, filter_value in filters.items(): 704 | field_to_filter_names, filter_type = self._split_filtering(filter_param) 705 | filtering_models = [starting_model] 706 | filtering_field_names = [] 707 | for field_to_filter_name in field_to_filter_names: 708 | filtering_model_fields = filtering_models[-1].get_class_fields() 709 | field = filtering_model_fields[field_to_filter_name] 710 | if field.__class__ in [RedisForeignKey, RedisManyToMany]: 711 | filtering_field_names.append(field_to_filter_name) 712 | if field_to_filter_names.index(field_to_filter_name) == len(field_to_filter_names) - 1: 713 | key = json.dumps({ 714 | 'field_names': filtering_field_names, 715 | 'model_names': [model.__name__ for model in filtering_models] 716 | }) 717 | if key not in cleaned_filters.keys(): 718 | cleaned_filters[key] = {} 719 | if 'id' not in cleaned_filters[key].keys(): 720 | cleaned_filters[key]['id'] = {} 721 | cleaned_filters[key]['id']['in'] = get_ids_from_untyped_data(filter_value) 722 | else: 723 | filtering_models.append(field.model) 724 | else: 725 | key = json.dumps({ 726 | 'field_names': filtering_field_names, 727 | 'model_names': [model.__name__ for model in filtering_models] 728 | }) 729 | if key not in cleaned_filters.keys(): 730 | cleaned_filters[key] = {} 731 | if field_to_filter_name not in cleaned_filters[key].keys(): 732 | cleaned_filters[key][field_to_filter_name] = {} 733 | cleaned_filters[key][field_to_filter_name][filter_type] = filter_value 734 | 735 | return cleaned_filters 736 | 737 | def _split_filtering(self, filter_param): 738 | filter_field_name, filter_type = filter_param, 'exact' 739 | if '__' in filter_param: 740 | filter_param_split = filter_param.split('__') 741 | if filter_param_split[-1] in ['exact', 'iexact', 'contains', 'icontains', 'in', 'gt', 'gte', 'lt', 'lte', 742 | 'startswith', 'istartswith', 'endswith', 'iendswith', 'range', 'isnull']: 743 | fields_to_filter = filter_param_split[:-1] 744 | filter_type = filter_param_split[-1] 745 | else: 746 | fields_to_filter = filter_param_split 747 | else: 748 | fields_to_filter = [filter_field_name] 749 | return fields_to_filter, filter_type 750 | 751 | def _get_cleaned_filters_with_filtered_ids(self, cleaned_filters): 752 | cleaned_filters_with_filtered_ids = {} 753 | for relations_data, filter_data in cleaned_filters.items(): 754 | filtered_ids = [] 755 | filtering_model_name = json.loads(relations_data)['model_names'][-1] 756 | for field_name, filters in filter_data.items(): 757 | stored_data = self.fast_get_keys_values( 758 | f'{self.prefix}:{filtering_model_name}:*:{field_name}' 759 | ) 760 | model = self._get_registered_model_by_name(filtering_model_name) 761 | field_filtered_ids = [] 762 | for instance_key, instance_value in stored_data.items(): 763 | value = self._deserialize_instance_field(model, field_name, instance_value) 764 | allowed = ( 765 | self._filter_value(value, filter_type, filter_by) 766 | for filter_type, filter_by in filters.items() 767 | ) 768 | if all(allowed): 769 | field_filtered_ids.append(int(instance_key.split(':')[-2])) 770 | filtered_ids.append(field_filtered_ids) 771 | if filtered_ids: 772 | filtered_ids = list(reduce( 773 | lambda a, b: set(a) & set(b), 774 | filtered_ids 775 | )) 776 | cleaned_filters_with_filtered_ids[relations_data] = filtered_ids 777 | 778 | return cleaned_filters_with_filtered_ids 779 | 780 | def _filter_value(self, value, filter_type, filter_by): 781 | allowed = True 782 | if isinstance(filter_by, datetime.datetime): 783 | filter_by = filter_by.replace(tzinfo=pytz.UTC) 784 | if filter_type == 'exact': 785 | if value != filter_by: 786 | allowed = False 787 | elif filter_type == 'iexact': 788 | if value.lower() != filter_by.lower(): 789 | allowed = False 790 | elif filter_type == 'contains': 791 | if filter_by not in value: 792 | allowed = False 793 | elif filter_type == 'icontains': 794 | if filter_by.lower() not in value.lower(): 795 | allowed = False 796 | elif filter_type == 'in': 797 | if value not in filter_by: 798 | allowed = False 799 | elif filter_type == 'gt': 800 | if value <= filter_by: 801 | allowed = False 802 | elif filter_type == 'gte': 803 | if value < filter_by: 804 | allowed = False 805 | elif filter_type == 'lt': 806 | if value >= filter_by: 807 | allowed = False 808 | elif filter_type == 'lte': 809 | if value > filter_by: 810 | allowed = False 811 | elif filter_type == 'startswith': 812 | if not value.startswith(filter_by): 813 | allowed = False 814 | elif filter_type == 'istartswith': 815 | if not value.lower().startswith(filter_by.lower()): 816 | allowed = False 817 | elif filter_type == 'endswith': 818 | if not value.endswith(filter_by): 819 | allowed = False 820 | elif filter_type == 'iendswith': 821 | if not value.lower().endswith(filter_by.lower()): 822 | allowed = False 823 | elif filter_type == 'range': 824 | if value not in range(filter_by): 825 | allowed = False 826 | elif filter_type == 'isnull': 827 | if (value in ['null', None]) != filter_by: 828 | allowed = False 829 | return allowed 830 | 831 | def _get_starting_model_filtered_ids(self, cleaned_filters_with_filtered_ids): 832 | starting_filtered_ids = [] 833 | for relations_data, filtered_ids in cleaned_filters_with_filtered_ids.items(): 834 | allowed_ids = filtered_ids.copy() 835 | real_relations_data = json.loads(relations_data) 836 | while real_relations_data['field_names']: 837 | field_name = real_relations_data['field_names'].pop(-1) 838 | model_name = real_relations_data['model_names'].pop(-1) 839 | all_stored_model_fields = self.fast_get_keys_values(f'{self.prefix}:{model_name}:*:{field_name}') 840 | allowed_ids = [ 841 | int(instance_key.split(':')[-2]) 842 | for instance_key, instance_value in all_stored_model_fields.items() 843 | if int(instance_value) in allowed_ids 844 | ] 845 | starting_filtered_ids.append(allowed_ids) 846 | if starting_filtered_ids: 847 | starting_filtered_ids = sorted(list(reduce( 848 | lambda set_a, set_b: set(set(set_a) & set(set_b)), 849 | starting_filtered_ids.copy() 850 | ))) 851 | return starting_filtered_ids 852 | 853 | def _filter_field_name(self, field_name, value, raw_filters): 854 | allowed_list = [True] 855 | for filter_param in raw_filters.keys(): 856 | filter_by = raw_filters[filter_param] 857 | fields_to_filter, filter_type = self._split_filtering(filter_param) 858 | if field_name == fields_to_filter[0]: 859 | fields_to_filter = fields_to_filter[1:] 860 | allowed_list.append(self._filter(value, fields_to_filter, filter_type, filter_by)) 861 | allowed = all(allowed_list) 862 | return allowed 863 | 864 | def _filter(self, value, fields_to_filter, filter_type, filter_by): 865 | for field_to_filter in fields_to_filter: 866 | if value in ['null', None]: 867 | value = None 868 | else: 869 | try: 870 | value = value[field_to_filter] 871 | except BaseException as ex: 872 | print(f'Exception: {ex}\n' 873 | f'Info: {field_to_filter}, {value}\n' 874 | f'Maybe: deep filtering is not included on this model') 875 | value = None 876 | if isinstance(value, datetime.datetime) and isinstance(filter_by, datetime.datetime): 877 | value = value.replace(tzinfo=pytz.UTC) 878 | filter_by = filter_by.replace(tzinfo=pytz.UTC) 879 | try: 880 | allowed = self._filter_value(value, filter_type, filter_by) 881 | except: 882 | allowed = False 883 | return allowed 884 | 885 | ### UPDATE ### 886 | 887 | def update(self, model, instances=None, return_dict=False, **fields_to_update): 888 | updated_instances, data_to_update = self._collect_update(model, instances, fields_to_update) 889 | self._confirm_update(data_to_update) 890 | result = self._return_with_format(updated_instances, return_dict) 891 | return result 892 | 893 | def update_nb(self, model, instances=None, return_dict=False, **fields_to_update): 894 | updated_instances, data_to_update = self._collect_update(model, instances, fields_to_update) 895 | asyncio.get_event_loop().create_task( 896 | self._confirm_update_async(data_to_update) 897 | ) 898 | result = self._return_with_format(updated_instances, return_dict) 899 | return result 900 | 901 | def _collect_update(self, model, instances, fields_to_update): 902 | model_name = model.__name__ 903 | if instances is not None: 904 | ids_to_update = get_ids_from_untyped_data(instances) 905 | else: 906 | ids_to_update = None 907 | if ids_to_update is not None: 908 | updated_instances = self.get(model, return_dict=True, id__in=ids_to_update) 909 | else: 910 | updated_instances = self.get(model, return_dict=True) 911 | 912 | collected_data_to_update = {} 913 | if self.save_type == 'fields': 914 | for field_to_update_name, field_to_update_value in fields_to_update.items(): 915 | saved_field_instance = self._get_field_instance_by_name(model, field_to_update_name) 916 | saved_field_instance.value = field_to_update_value 917 | saved_field_instance.clean() 918 | cleaned_field_to_update_value = saved_field_instance.value 919 | updated_instances = { 920 | updated_instance_id: {**updated_instance_data, field_to_update_name: field_to_update_value} 921 | for updated_instance_id, updated_instance_data in updated_instances.items() 922 | } 923 | keys_to_update = self.collect_keys(model_name, ids_to_update, field_to_update_name) 924 | update_mapping = { 925 | key: cleaned_field_to_update_value 926 | for key in keys_to_update 927 | } 928 | collected_data_to_update = { 929 | **collected_data_to_update, 930 | **update_mapping 931 | } 932 | elif self.save_type == 'instances': 933 | instance_keys_to_update = self._get_instance_keys_to_update(instances, model_name) 934 | for instance_key in instance_keys_to_update: 935 | prefix, model_name, instance_id = instance_key.split(':') 936 | instance_id = int(instance_id) 937 | fields_to_write = self._update_serialize_fields(instance_key, model, fields_to_update) 938 | collected_data_to_update[instance_key] = fields_to_write 939 | updated_instances[instance_id] = fields_to_write 940 | return updated_instances, collected_data_to_update 941 | 942 | def _get_instance_keys_to_update(self, instances, model_name): 943 | keys_to_update = [] 944 | if instances is None: 945 | keys_to_update += list(self.fast_get_keys(f'{self.prefix}:{model_name}:*')) 946 | else: 947 | ids_to_update = get_ids_from_untyped_data(instances) 948 | for instance_id in ids_to_update: 949 | keys_to_update += list(self.fast_get_keys(f'{self.prefix}:{model_name}:{instance_id}')) 950 | return keys_to_update 951 | 952 | def _update_serialize_fields(self, instance_key, model, fields_to_update): 953 | instance_data_json = self.redis_instance.get(instance_key) 954 | instance_data = json.loads(instance_data_json) 955 | serialized_data = {} 956 | for field_name, field_data in instance_data.items(): 957 | saved_field_instance = self._get_field_instance_by_name(model, field_name) 958 | if field_name in fields_to_update.keys(): 959 | saved_field_instance.value = fields_to_update[field_name] 960 | cleaned_value = saved_field_instance.clean() 961 | else: 962 | cleaned_value = field_data 963 | serialized_data[field_name] = cleaned_value 964 | return serialized_data 965 | 966 | def _confirm_update(self, data_to_update): 967 | if data_to_update.keys(): 968 | if self.save_type == 'fields': 969 | self.redis_instance.mset(data_to_update) 970 | elif self.save_type == 'instances': 971 | for instance_key, fields_to_update in data_to_update.items(): 972 | fields_to_write_json = json.dumps(fields_to_update) 973 | self.redis_instance.set(instance_key, fields_to_write_json) 974 | 975 | async def _confirm_update_async(self, data_to_update): 976 | self._confirm_update(data_to_update) 977 | 978 | 979 | 980 | # 981 | # 982 | # def _get_instance_keys_to_update(self, instances, model_name): 983 | # keys_to_update = [] 984 | # 985 | # if self.use_keys: 986 | # if instances is None: 987 | # keys_to_update += list(self.redis_instance.keys(f'{self.prefix}:{model_name}:*')) 988 | # else: 989 | # ids_to_update = get_ids_from_untyped_data(instances) 990 | # for instance_id in ids_to_update: 991 | # keys_to_update += list(self.redis_instance.keys(f'{self.prefix}:{model_name}:{instance_id}:*')) 992 | # else: 993 | # if instances is None: 994 | # keys_to_update += list(self.redis_instance.scan_iter(f'{self.prefix}:{model_name}:*')) 995 | # else: 996 | # ids_to_update = get_ids_from_untyped_data(instances) 997 | # for instance_id in ids_to_update: 998 | # keys_to_update += list(self.redis_instance.scan_iter(f'{self.prefix}:{model_name}:{instance_id}:*')) 999 | # return keys_to_update 1000 | 1001 | # def update(self, model, instances=None, return_dict=False, renew_ttl=False, new_ttl=None, **fields_to_update): 1002 | # model_name = model.__name__ 1003 | # instance_keys_to_update = self._get_instance_keys_to_update(instances, model_name) 1004 | # updated_instances = {} 1005 | # for instance_key in instance_keys_to_update: 1006 | # prefix, model_name, instance_id = instance_key.split(':') 1007 | # instance_id = int(instance_id) 1008 | # fields_to_write = self._update_serialize_fields(instance_key, model, fields_to_update) 1009 | # self._update_confirm(model, instance_key, renew_ttl, new_ttl, fields_to_write) 1010 | # updated_instances[instance_id] = fields_to_write 1011 | # result = self._return_with_format(updated_instances, return_dict) 1012 | # return result 1013 | # 1014 | # def update_nb(self, model, instances=None, return_dict=False, renew_ttl=False, new_ttl=None, **fields_to_update): 1015 | # model_name = model.__name__ 1016 | # instance_keys_to_update = self._get_instance_keys_to_update(instances, model_name) 1017 | # updated_instances = {} 1018 | # loop = asyncio.get_event_loop() 1019 | # for instance_key in instance_keys_to_update: 1020 | # prefix, model_name, instance_id = instance_key.split(':') 1021 | # instance_id = int(instance_id) 1022 | # fields_to_write = self._update_serialize_fields(instance_key, model, fields_to_update) 1023 | # loop.create_task( 1024 | # self._update_confirm_async(model, instance_key, renew_ttl, new_ttl, fields_to_write) 1025 | # ) 1026 | # updated_instances[instance_id] = fields_to_write 1027 | # result = self._return_with_format(updated_instances, return_dict) 1028 | # return result 1029 | # 1030 | 1031 | # 1032 | # def _update_serialize_fields(self, instance_key, model, fields_to_update): 1033 | # instance_data_json = self.redis_instance.get(instance_key) 1034 | # instance_data = json.loads(instance_data_json) 1035 | # serialized_data = {} 1036 | # for field_name, field_data in instance_data.items(): 1037 | # saved_field_instance = self._get_field_instance_by_name(field_name, model) 1038 | # if field_name in fields_to_update.keys(): 1039 | # saved_field_instance.value = fields_to_update[field_name] 1040 | # cleaned_value = saved_field_instance.clean() 1041 | # else: 1042 | # cleaned_value = field_data 1043 | # serialized_data[field_name] = cleaned_value 1044 | # return serialized_data 1045 | # 1046 | # def _update_confirm(self, model, instance_key, renew_ttl, new_ttl, fields_to_write): 1047 | # ttl = None 1048 | # if renew_ttl: 1049 | # ttl = model.get_instance_ttl() 1050 | # elif new_ttl: 1051 | # ttl = new_ttl 1052 | # fields_to_write_json = json.dumps(fields_to_write) 1053 | # self.redis_instance.set(instance_key, fields_to_write_json, ex=ttl) 1054 | # 1055 | # async def _update_confirm_async(self, model, instance_key, renew_ttl, new_ttl, fields_to_write): 1056 | # ttl = None 1057 | # if renew_ttl: 1058 | # ttl = model.get_instance_ttl() 1059 | # elif new_ttl: 1060 | # ttl = new_ttl 1061 | # fields_to_write_json = json.dumps(fields_to_write) 1062 | # self.redis_instance.set(instance_key, fields_to_write_json, ex=ttl) 1063 | 1064 | ### DELETE ### 1065 | 1066 | def delete(self, model, instances=None): 1067 | model_name = model.__name__ 1068 | self._confirm_delete(model_name, instances) 1069 | 1070 | def delete_nb(self, model, instances=None): 1071 | model_name = model.__name__ 1072 | asyncio.get_event_loop().create_task( 1073 | self._confirm_delete_async(model_name, instances) 1074 | ) 1075 | 1076 | def _confirm_delete(self, model_name, instances): 1077 | if self.save_type == 'fields': 1078 | ids_to_delete = None 1079 | if instances is not None: 1080 | ids_to_delete = get_ids_from_untyped_data(instances) 1081 | keys_to_delete = self.collect_keys(model_name, ids_to_delete) 1082 | if keys_to_delete: 1083 | self.redis_instance.delete(*keys_to_delete) 1084 | elif self.save_type == 'instances': 1085 | if instances is None: 1086 | delete_keys = self.fast_get_keys(f'{self.prefix}:{model_name}:*') 1087 | if delete_keys: 1088 | self.redis_instance.delete(*delete_keys) 1089 | else: 1090 | ids_to_delete = get_ids_from_untyped_data(instances) 1091 | for instance_id in ids_to_delete: 1092 | delete_keys = self.fast_get_keys(f'{self.prefix}:{model_name}:{instance_id}') 1093 | if delete_keys: 1094 | self.redis_instance.delete(*delete_keys) 1095 | 1096 | async def _confirm_delete_async(self, model_name, instances): 1097 | self._confirm_delete(model_name, instances) 1098 | 1099 | 1100 | ### CREATE ### 1101 | 1102 | def create(self, model, **params): 1103 | params = self._get_allowed_model_params(model, params) 1104 | redis_instance = model(redis_root=self, **params).save() 1105 | return redis_instance 1106 | 1107 | def create_nb(self, model, **params): 1108 | params = self._get_allowed_model_params(model, params) 1109 | redis_instance = model(redis_root=self, **params).save_nb() 1110 | return redis_instance 1111 | 1112 | def _get_allowed_model_params(self, model, params): 1113 | model_attrs = model.get_class_fields() 1114 | allowed_params = { 1115 | param_name: params[param_name] 1116 | for param_name in params.keys() 1117 | if param_name in model_attrs.keys() 1118 | } 1119 | return allowed_params 1120 | 1121 | ### HELPERS ### 1122 | 1123 | def _return_with_format(self, instances, return_dict=False): 1124 | if return_dict: 1125 | return instances 1126 | else: 1127 | instances_list = [ 1128 | { 1129 | 'id': instance_id, 1130 | **instance_fields 1131 | } 1132 | for instance_id, instance_fields in instances.items() 1133 | ] 1134 | return instances_list 1135 | 1136 | def fast_get_keys_values(self, string): 1137 | keys = self.fast_get_keys(string) 1138 | values = self.redis_instance.mget(keys) 1139 | results = dict(zip(keys, values)) 1140 | return results 1141 | 1142 | def fast_get_keys(self, string): 1143 | if self.use_keys: 1144 | keys = list(self.redis_instance.keys(string)) 1145 | else: 1146 | keys = list(self.redis_instance.scan_iter(string)) 1147 | return keys 1148 | 1149 | def collect_keys(self, model_name, ids=None, field_name=None): 1150 | collected_keys = [] 1151 | field_name_query_string = '*' if field_name is None else field_name 1152 | if ids is None: 1153 | query_string = f'{self.prefix}:{model_name}:*:{field_name_query_string}' 1154 | collected_keys = list(self.fast_get_keys(query_string)) 1155 | else: 1156 | for id in ids: 1157 | query_string = f'{self.prefix}:{model_name}:{id}:{field_name_query_string}' 1158 | collected_keys += list(self.fast_get_keys(query_string)) 1159 | return collected_keys 1160 | 1161 | 1162 | ### REDIS MODEL ### 1163 | 1164 | 1165 | class RedisModel: 1166 | id = RedisId() 1167 | 1168 | ### INIT ### 1169 | 1170 | def __init__(self, redis_root=None, **kwargs): 1171 | self.__model_data__ = { 1172 | 'redis_root': None, 1173 | 'name': None, 1174 | 'fields': {}, 1175 | 'meta': {}, 1176 | } 1177 | 1178 | if isinstance(redis_root, RedisRoot): 1179 | self.__model_data__['redis_root'] = redis_root 1180 | self.__model_data__['name'] = self.__class__.__name__ 1181 | if self.__class__ != RedisModel: 1182 | self._renew_fields() 1183 | self.__model_data__['redis_root'].register_models([self.__class__]) 1184 | self._fill_fields_values(kwargs) 1185 | 1186 | else: 1187 | raise Exception(f'{redis_root.__name__} type is {type(redis_root)}. Allowed only RedisRoot') 1188 | 1189 | def _renew_fields(self): 1190 | class_fields = self.__class__.get_class_fields() 1191 | fields = {} 1192 | for field_name, field in class_fields.items(): 1193 | fields[field_name] = self._get_initial_model_field(field_name) 1194 | self.__model_data__['fields'] = fields 1195 | 1196 | @classmethod 1197 | def get_class_fields(cls): 1198 | field_names = dir(cls) 1199 | fields = {} 1200 | for field_name in field_names: 1201 | field_value = getattr(cls, field_name) 1202 | if isinstance(field_value, RedisField): 1203 | fields[field_name] = field_value 1204 | return fields 1205 | 1206 | def _get_initial_model_field(self, field_name): 1207 | name = self.get('name') 1208 | if field_name in dir(self.__class__): 1209 | return deepcopy(getattr(self.__class__, field_name)) 1210 | else: 1211 | raise Exception(f'{name} has no field {field_name}') 1212 | 1213 | def _fill_fields_values(self, field_values_dict): 1214 | for name, value in field_values_dict.items(): 1215 | fields = self.__model_data__['fields'] 1216 | if name in fields.keys(): 1217 | fields[name].value = value 1218 | else: 1219 | raise Exception(f'{self.__class__.__name__} has no field {name}') 1220 | 1221 | ### SAVE ### 1222 | 1223 | def save(self): 1224 | instance_key, fields_dict, deserialized_fields = self._serialize_data() 1225 | self._set_fields(instance_key, fields_dict) 1226 | return deserialized_fields 1227 | 1228 | def save_nb(self): 1229 | instance_key, fields_dict, deserialized_fields = self._serialize_data() 1230 | asyncio.get_event_loop().create_task( 1231 | self._set_fields_async(instance_key, fields_dict) 1232 | ) 1233 | return deserialized_fields 1234 | 1235 | def _serialize_data(self): 1236 | redis_root = self.get('redis_root') 1237 | name = self.get('name') 1238 | fields = self.get('fields') 1239 | fields = dict(fields) 1240 | self._get_and_reserve_new_id() 1241 | fields['id'] = self.id 1242 | instance_key = f'{redis_root.prefix}:{name}:{self.id.value}' 1243 | deserialized_fields = {} 1244 | cleaned_fields = {} 1245 | for field_name, field in fields.items(): 1246 | try: 1247 | cleaned_value = field.clean() 1248 | cleaned_fields[field_name] = cleaned_value 1249 | deserialized_value = redis_root._deserialize_instance_field(self, field_name, cleaned_value) 1250 | deserialized_fields[field_name] = deserialized_value 1251 | except BaseException as ex: 1252 | raise Exception(f'{ex} ({name} -> {field_name})') 1253 | return instance_key, cleaned_fields, deserialized_fields 1254 | 1255 | def _get_and_reserve_new_id(self): 1256 | redis_root = self.get('redis_root') 1257 | self.id.value = redis_root.get_and_reserve_new_id(self.__class__) 1258 | 1259 | def _set_fields(self, instance_key, fields_dict): 1260 | redis_root = self.get('redis_root') 1261 | prefix, model_name, instance_id = instance_key.split(':') 1262 | instance_id = int(instance_id) 1263 | if redis_root.save_type == 'fields': 1264 | fields_dict = { 1265 | f'{instance_key}:{field_name}': field_value 1266 | for field_name, field_value in fields_dict.copy().items() 1267 | } 1268 | redis_root.redis_instance.mset(fields_dict) 1269 | elif redis_root.save_type == 'instances': 1270 | fields_data = json.dumps(fields_dict) 1271 | redis_root.redis_instance.set(instance_key, fields_data) 1272 | redis_root.remove_creating(self.__class__, instance_id) 1273 | 1274 | async def _set_fields_async(self, instance_key, fields_dict): 1275 | self._set_fields(instance_key, fields_dict) 1276 | 1277 | ### UTILS ### 1278 | 1279 | def set(self, force=False, **fields_with_values): 1280 | name = self.get('name') 1281 | fields = self.get('fields') 1282 | meta = self.get('meta') 1283 | for field_name, value in fields_with_values.items(): 1284 | if field_name in fields.keys(): 1285 | field = fields[field_name] 1286 | field.value = value 1287 | return field.value 1288 | elif field_name in meta.keys(): 1289 | meta[field_name] = value 1290 | return meta[field_name] 1291 | else: 1292 | if force: 1293 | fields[field_name] = value 1294 | else: 1295 | raise Exception(f'{name} has no field {field_name}') 1296 | 1297 | def get(self, field_name): 1298 | data = self.__model_data__ 1299 | fields = data['fields'] 1300 | meta = data['meta'] 1301 | redis_root = data['redis_root'] 1302 | name = data['name'] 1303 | if field_name in fields.keys(): 1304 | field = fields[field_name] 1305 | return field.value 1306 | elif field_name in meta.keys(): 1307 | return meta[field_name] 1308 | elif field_name == 'redis_root': 1309 | return redis_root 1310 | elif field_name == 'fields': 1311 | return fields 1312 | elif field_name == 'name': 1313 | return name 1314 | elif field_name == 'meta': 1315 | return meta 1316 | else: 1317 | raise Exception(f'{name} has no field {field_name}') 1318 | 1319 | -------------------------------------------------------------------------------- /python_redis_orm/tests/full_test.py: -------------------------------------------------------------------------------- 1 | import random 2 | import sys 3 | from time import sleep 4 | import asyncio 5 | import os 6 | 7 | 8 | from python_redis_orm.core import * 9 | 10 | 11 | def generate_token(chars_count): 12 | allowed_chars = 'QWERTYUIOPASDFGHJKLZXCVBNM1234567890' 13 | token = f'{"".join([random.choice(allowed_chars) for i in range(chars_count)])}' 14 | return token 15 | 16 | 17 | def generate_token_12_chars(): 18 | return generate_token(12) 19 | 20 | 21 | class BotSession(RedisModel): 22 | session_token = RedisString(default=generate_token_12_chars) 23 | created = RedisDateTime(default=datetime.datetime.now) 24 | 25 | 26 | class TaskChallenge(RedisModel): 27 | bot_session = RedisForeignKey(model=BotSession) 28 | task_id = RedisNumber(default=0, null=False) 29 | status = RedisString(default='in_work', choices={ 30 | 'in_work': 'В работе', 31 | 'completed': 'Завершён успешно', 32 | 'completed_frozen_points': 'Завершён успешно, получил поинты в холде', 33 | 'completed_points': 'Завершён успешно, получил поинты', 34 | 'completed_decommissioning': 'Завершён успешно, поинты списаны', 35 | 'failed_bot': 'Зафейлил бот', 36 | 'failed_task_creator': 'Зафейлил создатель задания', 37 | }, null=False) 38 | account_checks_count = RedisNumber(default=0) 39 | created = RedisDateTime(default=datetime.datetime.now) 40 | 41 | 42 | class DictCheckModel(RedisModel): 43 | redis_dict = RedisDict() 44 | 45 | 46 | class ListCheckModel(RedisModel): 47 | redis_list = RedisList() 48 | 49 | 50 | class ForeignKeyCheckModel(RedisModel): 51 | task_challenge = RedisForeignKey(model=TaskChallenge) 52 | 53 | 54 | class ManyToManyCheckModel(RedisModel): 55 | task_challenges = RedisManyToMany(model=TaskChallenge) 56 | 57 | 58 | class ModelWithOverriddenSave(RedisModel): 59 | multiplied_max_field = RedisNumber() 60 | 61 | def save(self): 62 | redis_root = self.get('redis_root') # get value of any field 63 | new_value = 1 64 | all_instances = redis_root.get(ModelWithOverriddenSave) 65 | if all_instances: 66 | max_value = max(map(lambda instance: instance['multiplied_max_field'], all_instances)) 67 | new_value = max_value * 2 68 | self.set(multiplied_max_field=new_value) 69 | return super().save() 70 | 71 | 72 | class SomeAbstractModel(RedisModel): 73 | abstract_field = RedisString(default='hello') 74 | 75 | 76 | class InheritanceTestModel(SomeAbstractModel): 77 | some_field = RedisBool(default=True) 78 | 79 | 80 | def clean_db_after_test(connection_pool, prefix): 81 | redis_instance = redis.Redis(connection_pool=connection_pool) 82 | keys_to_delete = [ 83 | *list(redis_instance.keys(f'{prefix}*')), 84 | *list(redis_instance.keys(f'*{prefix}')), 85 | *list(redis_instance.keys(f'*{prefix}*')), 86 | ] 87 | if keys_to_delete: 88 | redis_instance.delete(*keys_to_delete) 89 | 90 | 91 | def basic_test(connection_pool, prefix): 92 | try: 93 | redis_root = RedisRoot( 94 | prefix=prefix, 95 | connection_pool=connection_pool, 96 | ignore_deserialization_errors=True 97 | ) 98 | redis_root.register_models([ 99 | TaskChallenge, 100 | ]) 101 | count = 5 102 | for i in range(count): 103 | TaskChallenge( 104 | redis_root=redis_root, 105 | status='in_work', 106 | ).save() 107 | task_challenges_without_keys = redis_root.get(TaskChallenge) 108 | task_challenges_with_keys = redis_root.get(TaskChallenge, return_dict=True) 109 | have_exception = False 110 | if not len(task_challenges_without_keys) == count: 111 | have_exception = True 112 | if not len(task_challenges_with_keys) == count: 113 | have_exception = True 114 | else: 115 | if not task_challenges_with_keys.keys(): 116 | have_exception = True 117 | else: 118 | if len(list(task_challenges_with_keys.keys())) != len(task_challenges_without_keys): 119 | have_exception = True 120 | except BaseException as ex: 121 | have_exception = True 122 | print(ex) 123 | clean_db_after_test(connection_pool, prefix) 124 | return have_exception 125 | 126 | 127 | def auto_reg_test(connection_pool, prefix): 128 | redis_root = RedisRoot( 129 | prefix=prefix, 130 | connection_pool=connection_pool, 131 | ignore_deserialization_errors=True 132 | ) 133 | task_challenge_1 = TaskChallenge( 134 | redis_root=redis_root, 135 | status='in_work', 136 | ).save() 137 | try: 138 | task_challenges = redis_root.get(TaskChallenge) 139 | have_exception = False 140 | except BaseException as ex: 141 | have_exception = True 142 | clean_db_after_test(connection_pool, prefix) 143 | return have_exception 144 | 145 | 146 | def no_connection_pool_test(*args, **kwargs): 147 | try: 148 | redis_root = RedisRoot( 149 | ignore_deserialization_errors=True 150 | ) 151 | task_challenge_1 = TaskChallenge( 152 | redis_root=redis_root, 153 | status='in_work', 154 | ) 155 | task_challenge_1.save() 156 | task_challenges = redis_root.get(TaskChallenge) 157 | have_exception = False 158 | connection_pool = redis.ConnectionPool( 159 | host=os.environ['REDIS_HOST'], 160 | port=os.environ['REDIS_PORT'], 161 | db=0, 162 | decode_responses=True 163 | ) 164 | clean_db_after_test(connection_pool, redis_root.prefix) 165 | except BaseException as ex: 166 | have_exception = True 167 | return have_exception 168 | 169 | 170 | def choices_test(connection_pool, prefix): 171 | redis_root = RedisRoot( 172 | prefix=prefix, 173 | connection_pool=connection_pool, 174 | ignore_deserialization_errors=True 175 | ) 176 | task_challenge_1 = TaskChallenge( 177 | redis_root=redis_root, 178 | status='bruh', 179 | ) 180 | try: 181 | save_result = task_challenge_1.save() 182 | task_challenges = redis_root.get(TaskChallenge) 183 | have_exception = True 184 | except BaseException as ex: 185 | have_exception = False 186 | clean_db_after_test(connection_pool, prefix) 187 | return have_exception 188 | 189 | 190 | def order_test(connection_pool, prefix): 191 | redis_root = RedisRoot( 192 | prefix=prefix, 193 | connection_pool=connection_pool, 194 | ignore_deserialization_errors=True 195 | ) 196 | for i in range(3): 197 | TaskChallenge( 198 | redis_root=redis_root 199 | ).save() 200 | have_exception = True 201 | try: 202 | task_challenges = redis_root.get(TaskChallenge) 203 | first_task_challenge = redis_root.order(task_challenges, 'id')[0] 204 | last_task_challenge = redis_root.order(task_challenges, '-id')[0] 205 | if first_task_challenge['id'] == 1 and last_task_challenge['id'] == len(task_challenges): 206 | have_exception = False 207 | except BaseException as ex: 208 | pass 209 | clean_db_after_test(connection_pool, prefix) 210 | return have_exception 211 | 212 | 213 | def filter_test(connection_pool, prefix): 214 | redis_root = RedisRoot( 215 | prefix=prefix, 216 | connection_pool=connection_pool, 217 | ignore_deserialization_errors=True 218 | ) 219 | have_exception = True 220 | try: 221 | same_tokens_count = 2 222 | random_tokens_count = 8 223 | same_token = generate_token(50) 224 | random_tokens = [generate_token(50) for i in range(random_tokens_count)] 225 | for i in range(same_tokens_count): 226 | BotSession(redis_root, session_token=same_token).save() 227 | for random_token in random_tokens: 228 | BotSession(redis_root, session_token=random_token).save() 229 | yesterday = datetime.datetime.now() - datetime.timedelta(days=1) 230 | task_challenges_with_same_token = redis_root.get(BotSession, session_token=same_token, created__gte=yesterday) 231 | if len(task_challenges_with_same_token) == same_tokens_count: 232 | have_exception = False 233 | except BaseException as ex: 234 | print(ex) 235 | clean_db_after_test(connection_pool, prefix) 236 | return have_exception 237 | 238 | 239 | def update_test(connection_pool, prefix): 240 | redis_root = RedisRoot( 241 | prefix=prefix, 242 | connection_pool=connection_pool, 243 | ignore_deserialization_errors=True 244 | ) 245 | have_exception = True 246 | try: 247 | bot_session_1 = BotSession(redis_root, session_token='123').save() 248 | bot_session_1_id = bot_session_1['id'] 249 | redis_root.update(BotSession, bot_session_1, session_token='234') 250 | bot_sessions_filtered = redis_root.get(BotSession, id=bot_session_1_id) 251 | if len(bot_sessions_filtered) == 1: 252 | bot_session_1_new = bot_sessions_filtered[0] 253 | if 'session_token' in bot_session_1_new.keys(): 254 | if bot_session_1_new['session_token'] == '234': 255 | have_exception = False 256 | except BaseException as ex: 257 | print(ex) 258 | 259 | clean_db_after_test(connection_pool, prefix) 260 | return have_exception 261 | 262 | 263 | def functions_like_defaults_test(connection_pool, prefix): 264 | redis_root = RedisRoot( 265 | prefix=prefix, 266 | connection_pool=connection_pool, 267 | ignore_deserialization_errors=True 268 | ) 269 | have_exception = False 270 | try: 271 | bot_session_1 = BotSession(redis_root).save() 272 | bot_session_2 = BotSession(redis_root).save() 273 | if bot_session_1.session_token == bot_session_2.session_token: 274 | have_exception = True 275 | except BaseException as ex: 276 | pass 277 | 278 | clean_db_after_test(connection_pool, prefix) 279 | return have_exception 280 | 281 | 282 | def redis_foreign_key_test(connection_pool, prefix): 283 | redis_root = RedisRoot( 284 | prefix=prefix, 285 | connection_pool=connection_pool, 286 | ignore_deserialization_errors=False 287 | ) 288 | have_exception = True 289 | try: 290 | 291 | bot_session_1 = BotSession( 292 | redis_root=redis_root, 293 | ).save() 294 | task_challenge_1 = TaskChallenge( 295 | redis_root=redis_root, 296 | bot_session=bot_session_1 297 | ).save() 298 | bot_sessions = redis_root.get(BotSession) 299 | bot_session = redis_root.order(bot_sessions, '-id')[0] 300 | task_challenges = redis_root.get(TaskChallenge) 301 | task_challenge = redis_root.order(task_challenges, '-id')[0] 302 | if type(task_challenge['bot_session']) == dict: 303 | if task_challenge['bot_session'] == bot_session: 304 | have_exception = False 305 | except BaseException as ex: 306 | print(ex) 307 | 308 | clean_db_after_test(connection_pool, prefix) 309 | return have_exception 310 | 311 | 312 | def delete_test(connection_pool, prefix): 313 | redis_root = RedisRoot( 314 | prefix=prefix, 315 | connection_pool=connection_pool, 316 | ignore_deserialization_errors=True 317 | ) 318 | have_exception = True 319 | try: 320 | bot_session_1 = BotSession( 321 | redis_root=redis_root, 322 | ).save() 323 | task_challenge_1 = TaskChallenge( 324 | redis_root=redis_root, 325 | bot_session=bot_session_1 326 | ).save() 327 | redis_root.delete(BotSession, bot_session_1) 328 | redis_root.delete(TaskChallenge, task_challenge_1) 329 | bot_sessions = redis_root.get(BotSession) 330 | task_challenges = redis_root.get(TaskChallenge) 331 | if len(bot_sessions) == 0 and len(task_challenges) == 0: 332 | have_exception = False 333 | except BaseException as ex: 334 | print(ex) 335 | 336 | clean_db_after_test(connection_pool, prefix) 337 | return have_exception 338 | 339 | 340 | def use_keys_test(connection_pool, prefix): 341 | have_exception = True 342 | try: 343 | 344 | redis_root = RedisRoot( 345 | prefix=prefix, 346 | connection_pool=connection_pool, 347 | ignore_deserialization_errors=True, 348 | use_keys=True 349 | ) 350 | started_in_keys = datetime.datetime.now() 351 | tests_count = 100 352 | for i in range(tests_count): 353 | task_challenge_1 = TaskChallenge( 354 | redis_root=redis_root, 355 | status='in_work', 356 | ).save() 357 | redis_root.update(TaskChallenge, task_challenge_1, account_checks_count=1) 358 | ended_in_keys = datetime.datetime.now() 359 | keys_time = (ended_in_keys - started_in_keys).total_seconds() 360 | clean_db_after_test(connection_pool, prefix) 361 | 362 | redis_root = RedisRoot( 363 | prefix=prefix, 364 | connection_pool=connection_pool, 365 | ignore_deserialization_errors=True, 366 | use_keys=False 367 | ) 368 | started_in_no_keys = datetime.datetime.now() 369 | for i in range(tests_count): 370 | task_challenge_1 = TaskChallenge( 371 | redis_root=redis_root, 372 | status='in_work', 373 | ).save() 374 | redis_root.update(TaskChallenge, task_challenge_1, account_checks_count=1) 375 | ended_in_no_keys = datetime.datetime.now() 376 | no_keys_time = (ended_in_no_keys - started_in_no_keys).total_seconds() 377 | clean_db_after_test(connection_pool, prefix) 378 | keys_percent = round((no_keys_time / keys_time - 1) * 100, 2) 379 | keys_symbol = ('+' if keys_percent > 0 else '') 380 | print(f'Keys usage gives {keys_symbol}{keys_percent}% efficiency') 381 | have_exception = False 382 | except BaseException as ex: 383 | print(ex) 384 | 385 | clean_db_after_test(connection_pool, prefix) 386 | return have_exception 387 | 388 | 389 | def dict_test(connection_pool, prefix): 390 | redis_root = RedisRoot( 391 | prefix=prefix, 392 | connection_pool=connection_pool, 393 | ignore_deserialization_errors=True 394 | ) 395 | have_exception = True 396 | try: 397 | some_dict = { 398 | 'age': 19, 399 | 'weed': True 400 | } 401 | DictCheckModel( 402 | redis_root=redis_root, 403 | redis_dict=some_dict 404 | ).save() 405 | dict_check_model_instance = redis_root.get(DictCheckModel)[0] 406 | if 'redis_dict' in dict_check_model_instance.keys(): 407 | if dict_check_model_instance['redis_dict'] == some_dict: 408 | have_exception = False 409 | except BaseException as ex: 410 | print(ex) 411 | 412 | clean_db_after_test(connection_pool, prefix) 413 | return have_exception 414 | 415 | 416 | def list_test(connection_pool, prefix): 417 | redis_root = RedisRoot( 418 | prefix=prefix, 419 | connection_pool=connection_pool, 420 | ignore_deserialization_errors=True 421 | ) 422 | have_exception = True 423 | try: 424 | some_list = [5, 9, 's', 4.5, False] 425 | ListCheckModel( 426 | redis_root=redis_root, 427 | redis_list=some_list 428 | ).save() 429 | list_check_model_instance = redis_root.get(ListCheckModel)[0] 430 | if 'redis_list' in list_check_model_instance.keys(): 431 | if list_check_model_instance['redis_list'] == some_list: 432 | have_exception = False 433 | except BaseException as ex: 434 | print(ex) 435 | 436 | clean_db_after_test(connection_pool, prefix) 437 | return have_exception 438 | 439 | 440 | def non_blocking_test(connection_pool, prefix): 441 | have_exception = True 442 | 443 | try: 444 | 445 | def task(data_count, use_non_blocking): 446 | connection_pool = redis.ConnectionPool( 447 | host=os.environ['REDIS_HOST'], 448 | port=os.environ['REDIS_PORT'], 449 | db=0, 450 | decode_responses=True 451 | ) 452 | redis_root = RedisRoot( 453 | prefix=prefix, 454 | connection_pool=connection_pool, 455 | ignore_deserialization_errors=True 456 | ) 457 | 458 | for i in range(data_count): 459 | redis_root.create( 460 | ListCheckModel, 461 | redis_list=['update_list'] 462 | ) 463 | redis_root.create( 464 | ListCheckModel, 465 | redis_list=['delete_list'] 466 | ) 467 | 468 | def create_list(): 469 | if use_non_blocking: 470 | list_check_model_instance = redis_root.create_nb( 471 | ListCheckModel, 472 | redis_list=['create_list'] 473 | ) 474 | else: 475 | list_check_model_instance = redis_root.create( 476 | ListCheckModel, 477 | redis_list=['create_list'] 478 | ) 479 | 480 | def update_list(): 481 | to_update = redis_root.get( 482 | ListCheckModel, 483 | redis_list=['update_list'] 484 | ) 485 | if use_non_blocking: 486 | updated_instance = redis_root.update_nb( 487 | ListCheckModel, 488 | to_update, 489 | redis_list=['now_updated_list'] 490 | ) 491 | else: 492 | updated_instance = redis_root.update( 493 | ListCheckModel, 494 | to_update, 495 | redis_list=['now_updated_list'] 496 | ) 497 | 498 | def delete_list(): 499 | to_delete = redis_root.get( 500 | ListCheckModel, 501 | redis_list=['delete_list'] 502 | ) 503 | if use_non_blocking: 504 | redis_root.delete_nb( 505 | ListCheckModel, 506 | to_delete, 507 | ) 508 | else: 509 | redis_root.delete( 510 | ListCheckModel, 511 | to_delete, 512 | ) 513 | 514 | tests = [ 515 | create_list, 516 | update_list, 517 | delete_list, 518 | ] 519 | for test in tests: 520 | for i in range(data_count): 521 | test() 522 | 523 | data_count = 100 524 | clean_db_after_test(connection_pool, prefix) 525 | nb_started_in = datetime.datetime.now() 526 | task(data_count, True) 527 | nb_ended_in = datetime.datetime.now() 528 | nb_time = (nb_ended_in - nb_started_in).total_seconds() 529 | clean_db_after_test(connection_pool, prefix) 530 | b_started_in = datetime.datetime.now() 531 | task(data_count, False) 532 | b_ended_in = datetime.datetime.now() 533 | b_time = (b_ended_in - b_started_in).total_seconds() 534 | clean_db_after_test(connection_pool, prefix) 535 | 536 | nb_percent = round((nb_time / b_time - 1) * 100, 2) 537 | nb_symbol = ('+' if nb_percent > 0 else '') 538 | print(f'Non blocking gives {nb_symbol}{nb_percent}% efficiency') 539 | have_exception = False 540 | except BaseException as ex: 541 | print(ex) 542 | 543 | clean_db_after_test(connection_pool, prefix) 544 | return have_exception 545 | 546 | 547 | def foreign_key_test(connection_pool, prefix): 548 | redis_root = RedisRoot( 549 | prefix=prefix, 550 | connection_pool=connection_pool, 551 | ignore_deserialization_errors=True, 552 | ) 553 | have_exception = False 554 | try: 555 | task_id = 12345 556 | task_challenge = TaskChallenge( 557 | redis_root=redis_root, 558 | task_id=task_id 559 | ).save() 560 | foreign_key_check_instance = redis_root.create( 561 | ForeignKeyCheckModel, 562 | task_challenge=task_challenge 563 | ) 564 | # Check really created 565 | task_challenge_qs = redis_root.get(TaskChallenge, task_id=task_id) 566 | if len(task_challenge_qs) != 1: 567 | have_exception = True 568 | else: 569 | task_challenge = task_challenge_qs[0] 570 | foreign_key_check_instance_qs = redis_root.get(ForeignKeyCheckModel, task_challenge=task_challenge) 571 | if len(foreign_key_check_instance_qs) != 1: 572 | have_exception = True 573 | else: 574 | foreign_key_check_instance = foreign_key_check_instance_qs[0] 575 | if foreign_key_check_instance['task_challenge']['task_id'] != task_id: 576 | have_exception = True 577 | except BaseException as ex: 578 | print(ex) 579 | have_exception = True 580 | 581 | clean_db_after_test(connection_pool, prefix) 582 | return have_exception 583 | 584 | 585 | def many_to_many_test(connection_pool, prefix): 586 | redis_root = RedisRoot( 587 | prefix=prefix, 588 | connection_pool=connection_pool, 589 | ignore_deserialization_errors=True, 590 | ) 591 | have_exception = False 592 | try: 593 | tasks_ids = set([random.randrange(0, 100) for i in range(10)]) 594 | task_challenges = [ 595 | TaskChallenge( 596 | redis_root=redis_root, 597 | task_id=task_id 598 | ).save() 599 | for task_id in tasks_ids 600 | ] 601 | many_to_many_check_instance = redis_root.create( 602 | ManyToManyCheckModel, 603 | task_challenges=task_challenges 604 | ) 605 | # Check really created 606 | many_to_many_check_instances_qs = redis_root.get(ManyToManyCheckModel) 607 | if len(many_to_many_check_instances_qs) != 1: 608 | have_exception = True 609 | else: 610 | many_to_many_check_instance = many_to_many_check_instances_qs[0] 611 | if set([ 612 | task_challenge['id'] 613 | for task_challenge in many_to_many_check_instance['task_challenges'] 614 | ]) != set([ 615 | task_challenge['id'] 616 | for task_challenge in task_challenges 617 | ]): 618 | have_exception = True 619 | except BaseException as ex: 620 | print(ex) 621 | have_exception = True 622 | 623 | clean_db_after_test(connection_pool, prefix) 624 | return have_exception 625 | 626 | 627 | def save_override_test(connection_pool, prefix): 628 | redis_root = RedisRoot( 629 | prefix=prefix, 630 | connection_pool=connection_pool, 631 | ignore_deserialization_errors=True, 632 | ) 633 | have_exception = False 634 | try: 635 | instance_1 = redis_root.create(ModelWithOverriddenSave) 636 | instance_2 = redis_root.create(ModelWithOverriddenSave) 637 | if instance_1['multiplied_max_field'] * 2 != instance_2['multiplied_max_field']: 638 | have_exception = True 639 | except BaseException as ex: 640 | print(ex) 641 | have_exception = True 642 | 643 | clean_db_after_test(connection_pool, prefix) 644 | return have_exception 645 | 646 | 647 | def inheritance_test(connection_pool, prefix): 648 | redis_root = RedisRoot( 649 | prefix=prefix, 650 | connection_pool=connection_pool, 651 | ignore_deserialization_errors=False, 652 | ) 653 | have_exception = False 654 | try: 655 | instance_1 = redis_root.create(InheritanceTestModel) 656 | instance_2 = redis_root.create(InheritanceTestModel, abstract_field='nice') 657 | instance_1 = redis_root.get(InheritanceTestModel, abstract_field='hello') 658 | instance_2 = redis_root.get(InheritanceTestModel, abstract_field='nice') 659 | if not instance_1 or not instance_2: 660 | have_exception = True 661 | except BaseException as ex: 662 | print(ex) 663 | have_exception = True 664 | 665 | clean_db_after_test(connection_pool, prefix) 666 | return have_exception 667 | 668 | 669 | def performance_test(connection_pool, prefix): 670 | have_exception = False 671 | 672 | try: 673 | 674 | def run_test(count): 675 | 676 | def test(count, **test_params): 677 | real_test_params = test_params 678 | 679 | def run_exact_test(redis_root, count, use_non_blocking): 680 | 681 | update_data = [ 682 | redis_root.create(TaskChallenge, account_checks_count=100) 683 | for i in range(count) 684 | ] 685 | 686 | started_in = datetime.datetime.now() 687 | if use_non_blocking: 688 | 689 | create_results = [ 690 | redis_root.create_nb(TaskChallenge) 691 | for i in range(count) 692 | ] 693 | update_results = [ 694 | redis_root.update_nb(TaskChallenge, update_data, account_checks_count=200) 695 | for i in range(count) 696 | ] 697 | else: 698 | create_results = [ 699 | redis_root.create(TaskChallenge) 700 | for i in range(count) 701 | ] 702 | update_results = [ 703 | redis_root.update(TaskChallenge, update_data, account_checks_count=200) 704 | for i in range(count) 705 | ] 706 | ended_in = datetime.datetime.now() 707 | 708 | time_took = (ended_in - started_in).total_seconds() 709 | fields_count = len(results[0].keys()) * count 710 | clean_db_after_test(connection_pool, prefix) 711 | return [time_took, count, fields_count] 712 | 713 | print(prefix) 714 | redis_root = RedisRoot( 715 | prefix=prefix, 716 | connection_pool=connection_pool, 717 | ignore_deserialization_errors=True, 718 | use_keys=real_test_params['use_keys'], 719 | solo_usage=real_test_params['solo_usage'], 720 | save_type=('fields' if real_test_params['use_fields'] else 'instances'), 721 | ) 722 | 723 | test_result = run_exact_test(redis_root, count, real_test_params['use_non_blocking']) 724 | 725 | return test_result 726 | 727 | test_confs = [] 728 | 729 | for use_keys_variant in [True, False]: 730 | for use_non_blocking_variant in [True, False]: 731 | for solo_usage_variant in [True, False]: 732 | for use_fields_variant in [True, False]: 733 | test_confs.append({ 734 | 'use_keys': use_keys_variant, 735 | 'use_non_blocking': use_non_blocking_variant, 736 | 'solo_usage': solo_usage_variant, 737 | 'use_fields': use_fields_variant, 738 | }) 739 | 740 | test_confs_results = [ 741 | test(count, **test_conf) 742 | for test_conf in test_confs 743 | ] 744 | 745 | print(f'\n\n\n' 746 | f'The performance test results on your machine:\n' 747 | f'Every test creates and updates {test_confs_results[0][1]} instances ({test_confs_results[0][2]} fields) of TaskChallenge model,\n' 748 | f'Here are the results:\n' 749 | f'\n') 750 | 751 | min_time = min(list(map(lambda result: result[0], test_confs_results))) 752 | min_conf_text = '' 753 | for i, test_confs_result in enumerate(test_confs_results): 754 | test_conf_text = ", ".join([f"{k} = {v}" for k, v in test_confs[i].items()]) 755 | print(f'The configuration: {test_conf_text} took {test_confs_result[0]}s') 756 | if test_confs_result[0] == min_time: 757 | min_conf_text = test_conf_text 758 | print(f'\n\n' 759 | f'The best configuration: {min_conf_text}\n') 760 | 761 | count = 1000 762 | run_test(count) 763 | except BaseException as ex: 764 | print(ex) 765 | have_exception = True 766 | 767 | clean_db_after_test(connection_pool, prefix) 768 | return have_exception 769 | 770 | 771 | def flood_performance_test(connection_pool, prefix): 772 | have_exception = False 773 | redis_root = RedisRoot( 774 | prefix=prefix, 775 | connection_pool=connection_pool, 776 | ignore_deserialization_errors=True, 777 | use_keys=True 778 | ) 779 | clean_db_after_test(connection_pool, prefix) 780 | execs_count = 6 781 | count = 100 782 | 783 | def run_flood_test(use_fields): 784 | clean_db_after_test(connection_pool, prefix) 785 | completion_time_list = [] 786 | for exec in range(execs_count): 787 | time_start = datetime.datetime.now() 788 | for i in range(count): 789 | task_challenges_qs = redis_root.create(TaskChallenge, account_checks_count=i) 790 | for i in range(count): 791 | task_challenges_qs = redis_root.get(TaskChallenge, account_checks_count=i) 792 | time_end = datetime.datetime.now() 793 | completion_time_list.append((time_end - time_start).total_seconds()) 794 | first_test = completion_time_list[0] 795 | last_test = completion_time_list[-1] 796 | print(f'\n' 797 | f'Use fields: {use_fields}\n' 798 | f'0 -> {(execs_count - 1) * count} stored instances changes execution time like:\n' 799 | f'{"s -> ".join((str(completion_time) for completion_time in completion_time_list))}s\n' 800 | f'+{(last_test / first_test - 1) * 100}% of time spent') 801 | clean_db_after_test(connection_pool, prefix) 802 | 803 | print('Running test, that floods your redis database and checking create and filter time...') 804 | run_flood_test(True) 805 | run_flood_test(False) 806 | return have_exception 807 | 808 | 809 | def run_tests(): 810 | connection_pool = redis.ConnectionPool( 811 | host=os.environ['REDIS_HOST'], 812 | port=os.environ['REDIS_PORT'], 813 | db=0, 814 | decode_responses=True 815 | ) 816 | tests = [ 817 | basic_test, 818 | auto_reg_test, 819 | no_connection_pool_test, 820 | choices_test, 821 | order_test, 822 | filter_test, 823 | functions_like_defaults_test, 824 | redis_foreign_key_test, 825 | update_test, 826 | delete_test, 827 | use_keys_test, 828 | list_test, 829 | dict_test, 830 | non_blocking_test, 831 | foreign_key_test, 832 | many_to_many_test, 833 | save_override_test, 834 | inheritance_test, 835 | performance_test, 836 | flood_performance_test, 837 | ] 838 | results = [] 839 | started_in = datetime.datetime.now() 840 | print('STARTING TESTS\n') 841 | for i, test in enumerate(tests): 842 | print(f'Starting {int(i + 1)} test: {test.__name__.replace("_", " ")}') 843 | test_started_in = datetime.datetime.now() 844 | result = not test(connection_pool, test.__name__) 845 | test_ended_in = datetime.datetime.now() 846 | test_time = (test_ended_in - test_started_in).total_seconds() 847 | print(f'{result = } / {test_time}s\n') 848 | results.append(result) 849 | ended_in = datetime.datetime.now() 850 | time = (ended_in - started_in).total_seconds() 851 | success_message = 'SUCCESS' if all(results) else 'FAILED' 852 | print('\n' 853 | f'{success_message}!\n') 854 | results_success_count = 0 855 | for i, result in enumerate(results): 856 | result_message = 'SUCCESS' if result else 'FAILED' 857 | print(f'Test {(i + 1)}/{len(results)}: {result_message} ({tests[i].__name__.replace("_", " ")})') 858 | if result: 859 | results_success_count += 1 860 | print(f'\n' 861 | f'{results_success_count} / {len(results)} tests ran successfully\n' 862 | f'All tests completed in {time}s\n') 863 | 864 | return all(results) 865 | 866 | 867 | if __name__ == '__main__': 868 | results = run_tests() 869 | if not results: 870 | sys.exit(1) -------------------------------------------------------------------------------- /python_redis_orm/utils.py: -------------------------------------------------------------------------------- 1 | from inspect import isfunction 2 | 3 | 4 | def check_types(value, allowed_types): 5 | if value: 6 | if not isinstance(value, allowed_types): 7 | allowed_types_text = allowed_types 8 | if isinstance(allowed_types, (list, set, tuple)): 9 | allowed_types_text = ", ".join([str(allowed_type.__name__) for allowed_type in allowed_types]) 10 | else: 11 | allowed_types_text = str(allowed_types) 12 | raise Exception(f'{value} has type: {value.__class__.__name__}. Allowed only: {allowed_types_text}') 13 | 14 | 15 | def check_classes(value, allowed_classes): 16 | if value: 17 | if not issubclass(value, allowed_classes): 18 | allowed_types_text = allowed_classes 19 | if isinstance(allowed_classes, (list, set, tuple)): 20 | allowed_types_text = ", ".join([str(allowed_class.__name__) for allowed_class in allowed_classes]) 21 | else: 22 | allowed_types_text = str(allowed_classes) 23 | raise Exception(f'{value} class is: {value.__class__.__name__}. Allowed subclasses: {allowed_types_text}') 24 | 25 | 26 | def get_ids_from_untyped_data(instances): 27 | if type(instances) == dict: 28 | if 'id' in instances.keys(): 29 | ids = [instances['id']] 30 | else: 31 | if all([ 32 | (type(instance_key) == int) 33 | for instance_key in instances.keys() 34 | ]): 35 | ids = list(set(instances.keys())) 36 | else: 37 | raise Exception('Not all keys are of the type int') 38 | elif type(instances) in [list, tuple, set]: 39 | if all([ 40 | (type(instance_id) == int) 41 | for instance_id in instances 42 | ]): 43 | ids = list(set(instances)) 44 | else: 45 | try: 46 | if all([ 47 | (type(instance_dict['id']) == int) 48 | for instance_dict in instances 49 | ]): 50 | ids = [ 51 | instance_dict['id'] 52 | for instance_dict in instances 53 | ] 54 | else: 55 | raise Exception('Not all elements are of the type int') 56 | except: 57 | raise Exception('Not all elements are of the type int') 58 | elif type(instances) == int: 59 | ids = [instances] 60 | else: 61 | raise Exception(f"Can't get ids from {instances}") 62 | return ids 63 | 64 | 65 | def check_callable(value): 66 | try: 67 | result_value = value() 68 | except: 69 | result_value = value 70 | return result_value 71 | 72 | 73 | def attr_is_real(attr_k, attr_v): 74 | return not isfunction(attr_v) and not (attr_k.startswith('__') and attr_k.endswith('__')) 75 | --------------------------------------------------------------------------------