django-cfg 1.4.117__py3-none-any.whl → 1.4.118__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of django-cfg might be problematic. Click here for more details.

django_cfg/__init__.py CHANGED
@@ -32,7 +32,7 @@ Example:
32
32
  default_app_config = "django_cfg.apps.DjangoCfgConfig"
33
33
 
34
34
  # Version information
35
- __version__ = "1.4.117"
35
+ __version__ = "1.4.118"
36
36
  __license__ = "MIT"
37
37
 
38
38
  # Import registry for organized lazy loading
@@ -166,6 +166,8 @@ class InstalledAppsBuilder:
166
166
  # Add django-q2 if enabled
167
167
  if hasattr(self.config, "django_q2") and self.config.django_q2 and self.config.django_q2.enabled:
168
168
  apps.append("django_q")
169
+ # Auto-add django_q2 module for automatic schedule synchronization
170
+ apps.append("django_cfg.modules.django_q2")
169
171
 
170
172
  # Add DRF Tailwind theme module (uses Tailwind via CDN)
171
173
  if self.config.enable_drf_tailwind:
@@ -302,6 +302,19 @@ class DjangoQ2Config(BaseModel):
302
302
  )
303
303
  ```
304
304
 
305
+ Schedule Synchronization (AUTOMATIC):
306
+ When Django-Q2 is enabled, schedules are automatically synced after migrations.
307
+
308
+ The module 'django_cfg.modules.django_q2' is automatically added to INSTALLED_APPS
309
+ when django_q2.enabled=True, so you don't need to add it manually.
310
+
311
+ It uses Django's post_migrate signal to sync schedules from config to database.
312
+
313
+ Manual sync (optional):
314
+ ```bash
315
+ python manage.py sync_django_q_schedules
316
+ ```
317
+
305
318
  Admin interface:
306
319
  - Visit /admin/django_q/ to view tasks and schedules
307
320
  - Monitor task execution, failures, and performance
@@ -0,0 +1,140 @@
1
+ # Django-Q2 Module
2
+
3
+ Автоматическая синхронизация расписаний Django-Q2 из конфига в базу данных.
4
+
5
+ ## Зачем это нужно?
6
+
7
+ Django-Q2 хранит расписания в базе данных, но **не создаёт их автоматически** из конфига.
8
+ Этот модуль решает эту проблему - синхронизирует расписания после каждой миграции.
9
+
10
+ ## Использование
11
+
12
+ ### 1. Включи Django-Q2 в конфиге
13
+
14
+ ```python
15
+ # config.py
16
+ from django_cfg.models.django import DjangoQ2Config
17
+
18
+ django_q2 = DjangoQ2Config(
19
+ enabled=True, # ← Автоматически добавит django_q и django_cfg.modules.django_q2 в INSTALLED_APPS
20
+ schedules=[...]
21
+ )
22
+ ```
23
+
24
+ **Модуль подключается автоматически!** Не нужно вручную добавлять в INSTALLED_APPS.
25
+
26
+ ### 2. Определи расписания в конфиге
27
+
28
+ ```python
29
+ from django_cfg.models.django import DjangoQ2Config, DjangoQ2ScheduleConfig
30
+
31
+ django_q2 = DjangoQ2Config(
32
+ enabled=True,
33
+ schedules=[
34
+ DjangoQ2ScheduleConfig(
35
+ name="Sync balances hourly",
36
+ schedule_type="hourly",
37
+ command="sync_account_balances",
38
+ command_args=["--verbose"],
39
+ ),
40
+ DjangoQ2ScheduleConfig(
41
+ name="Cleanup daily",
42
+ schedule_type="cron",
43
+ cron="0 2 * * *", # 2 AM каждый день
44
+ command="cleanup_old_data",
45
+ command_kwargs={"days": 30},
46
+ ),
47
+ ],
48
+ )
49
+ ```
50
+
51
+ ### 3. Запусти миграции
52
+
53
+ ```bash
54
+ python manage.py migrate
55
+ ```
56
+
57
+ **Вывод:**
58
+ ```
59
+ Running migrations:
60
+ ...
61
+ Syncing 2 Django-Q2 schedule(s)...
62
+ ✓ Created schedule: Sync balances hourly
63
+ ✓ Created schedule: Cleanup daily
64
+ ✅ Django-Q2 schedules synced: 2 created, 0 updated
65
+ ```
66
+
67
+ ### 4. Запусти qcluster
68
+
69
+ ```bash
70
+ python manage.py qcluster
71
+ ```
72
+
73
+ Готово! Расписания автоматически синхронизированы и работают.
74
+
75
+ ## Как это работает?
76
+
77
+ 1. **Модуль подключается** к сигналу `post_migrate`
78
+ 2. **После миграций** автоматически:
79
+ - Читает расписания из конфига
80
+ - Создаёт/обновляет их в базе данных (Schedule model)
81
+ 3. **Django-Q2 читает** расписания из базы и выполняет задачи
82
+
83
+ ## Ручная синхронизация (опционально)
84
+
85
+ Если нужно синхронизировать без миграций:
86
+
87
+ ```bash
88
+ python manage.py sync_django_q_schedules
89
+
90
+ # Или с --dry-run для проверки:
91
+ python manage.py sync_django_q_schedules --dry-run
92
+ ```
93
+
94
+ ## Безопасность
95
+
96
+ - ✅ **Идемпотентность**: можно запускать много раз, не создаст дубликаты
97
+ - ✅ **Без race conditions**: синхронизация происходит один раз за цикл миграций
98
+ - ✅ **Graceful degradation**: если Django-Q2 не установлен, модуль просто молча пропустит синхронизацию
99
+ - ✅ **Logging**: все операции логируются для отладки
100
+
101
+ ## Преимущества перед ручной синхронизацией
102
+
103
+ | Аспект | Ручная синхронизация | Модуль |
104
+ |--------|---------------------|--------|
105
+ | Автоматизация | Нужно помнить запускать | Автоматически |
106
+ | Деплой | Легко забыть | Всегда синхронизировано |
107
+ | CI/CD | Нужно добавлять в скрипты | Работает из коробки |
108
+ | Ошибки | Легко пропустить | Логи миграций |
109
+
110
+ ## Troubleshooting
111
+
112
+ ### Расписания не создаются
113
+
114
+ Проверь:
115
+ 1. Модуль добавлен в INSTALLED_APPS
116
+ 2. `django_q2.enabled = True` в конфиге
117
+ 3. В конфиге есть расписания
118
+ 4. Миграции запущены: `python manage.py migrate`
119
+
120
+ ### Расписания не обновляются
121
+
122
+ Запусти миграции повторно или используй ручную синхронизацию:
123
+ ```bash
124
+ python manage.py migrate --run-syncdb
125
+ # или
126
+ python manage.py sync_django_q_schedules
127
+ ```
128
+
129
+ ### Логи синхронизации
130
+
131
+ Включи DEBUG логи:
132
+ ```python
133
+ LOGGING = {
134
+ 'loggers': {
135
+ 'django_cfg.modules.django_q2': {
136
+ 'level': 'DEBUG',
137
+ },
138
+ },
139
+ }
140
+ ```
@@ -0,0 +1,8 @@
1
+ """
2
+ Django-Q2 module for django-cfg.
3
+
4
+ Provides automatic schedule synchronization from config to database.
5
+ """
6
+ from .apps import DjangoQ2ModuleConfig
7
+
8
+ __all__ = ['DjangoQ2ModuleConfig']
@@ -0,0 +1,107 @@
1
+ """
2
+ AppConfig for Django-Q2 module with automatic schedule synchronization.
3
+ """
4
+ import logging
5
+ from django.apps import AppConfig
6
+ from django.db.models.signals import post_migrate
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ def sync_schedules_after_migrate(sender, **kwargs):
12
+ """
13
+ Automatically sync Django-Q2 schedules after migrations.
14
+
15
+ This ensures schedules are always up-to-date after deployment.
16
+ Runs only once per migration cycle, safe from race conditions.
17
+ """
18
+ # Only run for the django_cfg_django_q2 app itself
19
+ if sender.name != 'django_cfg.modules.django_q2':
20
+ return
21
+
22
+ # Import here to avoid circular imports and ensure Django is ready
23
+ try:
24
+ from django_q.models import Schedule
25
+ from django_cfg.core.config import get_current_config
26
+ except ImportError as e:
27
+ logger.warning(f"Could not import Django-Q2 dependencies: {e}")
28
+ return
29
+
30
+ config = get_current_config()
31
+
32
+ if not config or not hasattr(config, 'django_q2') or not config.django_q2 or not config.django_q2.enabled:
33
+ logger.debug("Django-Q2 not enabled, skipping schedule sync")
34
+ return
35
+
36
+ enabled_schedules = config.django_q2.get_enabled_schedules()
37
+
38
+ if not enabled_schedules:
39
+ logger.debug("No Django-Q2 schedules found in config")
40
+ return
41
+
42
+ logger.info(f"Syncing {len(enabled_schedules)} Django-Q2 schedule(s)...")
43
+
44
+ created = 0
45
+ updated = 0
46
+
47
+ for schedule_config in enabled_schedules:
48
+ schedule_dict = schedule_config.to_django_q_format()
49
+ name = schedule_dict['name']
50
+
51
+ try:
52
+ schedule, created_flag = Schedule.objects.update_or_create(
53
+ name=name,
54
+ defaults=schedule_dict
55
+ )
56
+
57
+ if created_flag:
58
+ created += 1
59
+ logger.info(f" ✓ Created schedule: {name}")
60
+ else:
61
+ updated += 1
62
+ logger.debug(f" ✓ Updated schedule: {name}")
63
+
64
+ except Exception as e:
65
+ logger.error(f" ✗ Failed to sync schedule '{name}': {e}")
66
+
67
+ logger.info(f"✅ Django-Q2 schedules synced: {created} created, {updated} updated")
68
+
69
+
70
+ class DjangoQ2ModuleConfig(AppConfig):
71
+ """
72
+ AppConfig for Django-Q2 module.
73
+
74
+ Automatically syncs schedules from config to database after migrations.
75
+ This eliminates the need for manual `sync_django_q_schedules` command.
76
+
77
+ Features:
78
+ - Automatic schedule sync after migrations
79
+ - Safe from race conditions (runs only once)
80
+ - Logs all sync operations
81
+ - Gracefully handles missing dependencies
82
+
83
+ Usage:
84
+ Add to INSTALLED_APPS:
85
+ INSTALLED_APPS = [
86
+ ...
87
+ 'django_cfg.modules.django_q2', # Auto-syncs schedules
88
+ ]
89
+ """
90
+
91
+ default_auto_field = 'django.db.models.BigAutoField'
92
+ name = 'django_cfg.modules.django_q2'
93
+ verbose_name = 'Django-CFG Django-Q2 Module'
94
+
95
+ def ready(self):
96
+ """
97
+ Connect post_migrate signal to automatically sync schedules.
98
+
99
+ This runs after all migrations are complete, ensuring:
100
+ 1. Database tables exist
101
+ 2. Config is loaded
102
+ 3. Schedules are synced only once per migration cycle
103
+ """
104
+ # Connect the signal
105
+ post_migrate.connect(sync_schedules_after_migrate, sender=self)
106
+
107
+ logger.debug(f"{self.verbose_name} initialized - auto-sync enabled")
File without changes
@@ -0,0 +1,74 @@
1
+ """
2
+ Management command to sync Django-Q2 schedules from config.
3
+
4
+ Usage:
5
+ python manage.py sync_django_q_schedules # Create/update schedules
6
+ python manage.py sync_django_q_schedules --dry-run # Show what would be created
7
+ """
8
+ from django.core.management.base import BaseCommand
9
+ from django_q.models import Schedule
10
+ from django_cfg.core.config import get_current_config
11
+
12
+
13
+ class Command(BaseCommand):
14
+ help = 'Sync Django-Q2 schedules from config to database'
15
+
16
+ def add_arguments(self, parser):
17
+ parser.add_argument(
18
+ '--dry-run',
19
+ action='store_true',
20
+ help='Show what would be created without actually creating',
21
+ )
22
+
23
+ def handle(self, *args, **options):
24
+ config = get_current_config()
25
+
26
+ if not config:
27
+ self.stdout.write(self.style.ERROR('❌ No config found'))
28
+ return
29
+
30
+ # Check if Django-Q2 is enabled
31
+ if not hasattr(config, 'django_q2') or not config.django_q2 or not config.django_q2.enabled:
32
+ self.stdout.write(self.style.WARNING('⚠️ Django-Q2 is not enabled in config'))
33
+ return
34
+
35
+ enabled_schedules = config.django_q2.get_enabled_schedules()
36
+
37
+ if not enabled_schedules:
38
+ self.stdout.write(self.style.WARNING('⚠️ No schedules found in config'))
39
+ return
40
+
41
+ self.stdout.write(f'📋 Found {len(enabled_schedules)} schedule(s) in config\n')
42
+
43
+ created = 0
44
+ updated = 0
45
+
46
+ for schedule_config in enabled_schedules:
47
+ schedule_dict = schedule_config.to_django_q_format()
48
+ name = schedule_dict['name']
49
+
50
+ if options['dry_run']:
51
+ self.stdout.write(f' [DRY RUN] Would create/update: {name}')
52
+ continue
53
+
54
+ # Update or create schedule
55
+ schedule, created_flag = Schedule.objects.update_or_create(
56
+ name=name,
57
+ defaults=schedule_dict
58
+ )
59
+
60
+ if created_flag:
61
+ created += 1
62
+ self.stdout.write(self.style.SUCCESS(f' ✓ Created: {name}'))
63
+ else:
64
+ updated += 1
65
+ self.stdout.write(self.style.SUCCESS(f' ✓ Updated: {name}'))
66
+
67
+ if not options['dry_run']:
68
+ self.stdout.write(self.style.SUCCESS(
69
+ f'\n✅ Summary: {created} created, {updated} updated'
70
+ ))
71
+ else:
72
+ self.stdout.write(self.style.WARNING(
73
+ f'\n[DRY RUN] Would create {len(enabled_schedules)} schedule(s)'
74
+ ))
django_cfg/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "django-cfg"
7
- version = "1.4.117"
7
+ version = "1.4.118"
8
8
  description = "Modern Django framework with type-safe Pydantic v2 configuration, Next.js admin integration, real-time WebSockets, and 8 enterprise apps. Replace settings.py with validated models, 90% less code. Production-ready with AI agents, auto-generated TypeScript clients, and zero-config features."
9
9
  readme = "README.md"
10
10
  keywords = [ "django", "configuration", "pydantic", "settings", "type-safety", "pydantic-settings", "django-environ", "startup-validation", "ide-autocomplete", "nextjs-admin", "react-admin", "websocket", "centrifugo", "real-time", "typescript-generation", "ai-agents", "enterprise-django", "django-settings", "type-safe-config", "modern-django",]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-cfg
3
- Version: 1.4.117
3
+ Version: 1.4.118
4
4
  Summary: Modern Django framework with type-safe Pydantic v2 configuration, Next.js admin integration, real-time WebSockets, and 8 enterprise apps. Replace settings.py with validated models, 90% less code. Production-ready with AI agents, auto-generated TypeScript clients, and zero-config features.
5
5
  Project-URL: Homepage, https://djangocfg.com
6
6
  Project-URL: Documentation, https://djangocfg.com
@@ -1,5 +1,5 @@
1
1
  django_cfg/README.md,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- django_cfg/__init__.py,sha256=a-zgNY8EXo7-I6ZParGfaJYes0Qu_GNrYhqILSkLhAU,1621
2
+ django_cfg/__init__.py,sha256=czBPTVeDvyLUMK-04V3RRLhBFzXwK7DP_dDyzQq7ZNw,1621
3
3
  django_cfg/apps.py,sha256=72m3uuvyqGiLx6gOfE-BD3P61jddCCERuBOYpxTX518,1605
4
4
  django_cfg/config.py,sha256=xdwFE8bocOEnMjqDOwr1_M02oUgdaMG_2M6F_IuB9GQ,1351
5
5
  django_cfg/apps/__init__.py,sha256=JtDmEYt1OcleWM2ZaeX0LKDnRQzPOavfaXBWG4ECB5Q,26
@@ -544,7 +544,7 @@ django_cfg/core/backends/smtp.py,sha256=kWkNMG7UwLsHcFYSKRgrk1HbP9mU1fxzWYnalHXq
544
544
  django_cfg/core/base/__init__.py,sha256=Z3bZvxejxk4vvWqmqTBLUi9XJpo6A_5Bq4R0J8q81Y4,116
545
545
  django_cfg/core/base/config_model.py,sha256=fnmvjOs2fHTb8ifjFv0EsbVPFScG87ExikIqVAqpDZc,21810
546
546
  django_cfg/core/builders/__init__.py,sha256=jkInI7_jbxcjitapohw6QmbJPpacnnID6V1JovqtOFM,282
547
- django_cfg/core/builders/apps_builder.py,sha256=96o35vB7-eIzAv32bZ505H6lGmcxjqZmhUDsxbOQ4Ug,6675
547
+ django_cfg/core/builders/apps_builder.py,sha256=1C7M3Uq4aScuz4syuNGHyl3USus1h_FgULPfzKAerak,6810
548
548
  django_cfg/core/builders/middleware_builder.py,sha256=OwqQRoJKYWlXsQNPFBfUvVMYdppUHCPw-UDczV_esYg,3101
549
549
  django_cfg/core/builders/security_builder.py,sha256=W8Lk9eTMi3PuNK5qH-BIHegeE0cbvmuHKTumLcwOAh8,23961
550
550
  django_cfg/core/environment/__init__.py,sha256=sMOIe9z1i51j8B1VGjpLHJMaeDsBfsgn1TmL03FIeNo,141
@@ -644,7 +644,7 @@ django_cfg/models/django/__init__.py,sha256=J4VRl77vgXzfKcdUcX-sPxpBSOJ4qA-8RstP
644
644
  django_cfg/models/django/axes.py,sha256=-4nk2gSfpj7lNY5vnm_2jHVLz8VAKoEd9yF2TuCR8O8,5624
645
645
  django_cfg/models/django/constance.py,sha256=6x57bi40mDX0fKcKeQKgV2BO3WIVYPQllAchWsj4KvM,8847
646
646
  django_cfg/models/django/crypto_fields.py,sha256=OguITidM4Mp564p_gbsokeNCZxjL9hrK1Vw0McuA3yo,4700
647
- django_cfg/models/django/django_q2.py,sha256=le-Zjg2JjiFyfOJoHeODrjUylUBBm7eoneaGmVOmq1I,15067
647
+ django_cfg/models/django/django_q2.py,sha256=G8m2hg0-IE-GLsGabaHetGVdbb_cNas8RCbZBBr-xz4,15564
648
648
  django_cfg/models/django/environment.py,sha256=lBCHBs1lphv9tlu1BCTfLZeH_kUame0p66A_BIjBY7M,9440
649
649
  django_cfg/models/django/openapi.py,sha256=avE3iapaCj8eyOqVUum_v2EExR3V-hwHrexqtXMHtTQ,3739
650
650
  django_cfg/models/django/revolution_legacy.py,sha256=Z4SPUS7QSv62EuPAeFFoXGEgqLmdXnVEr7Ofk1IDtVc,8918
@@ -947,6 +947,12 @@ django_cfg/modules/django_ngrok/service.py,sha256=Xkh9Gl6Rth32UcT0UYjD0ckROHFw6F
947
947
  django_cfg/modules/django_ngrok/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
948
948
  django_cfg/modules/django_ngrok/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
949
949
  django_cfg/modules/django_ngrok/management/commands/runserver_ngrok.py,sha256=urwO1nPjcBMPn7k8WXBzfWQpjzCsIKaiET2Gf5afYwk,6259
950
+ django_cfg/modules/django_q2/README.md,sha256=pyY0EqefXTJ47k_liMv22d0zx-9dHVPGFUSZS3aLQjE,4763
951
+ django_cfg/modules/django_q2/__init__.py,sha256=6Eu26O-4ddwE8qO27pTT6x13-iPc9eA2Pzq8_cRHZMc,186
952
+ django_cfg/modules/django_q2/apps.py,sha256=UoSbow-Wlkwriem5mEud1vrUD7_TnqANMzoajGLBVQs,3396
953
+ django_cfg/modules/django_q2/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
954
+ django_cfg/modules/django_q2/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
955
+ django_cfg/modules/django_q2/management/commands/sync_django_q_schedules.py,sha256=ik_4kmKp9RUTOaxewWK6KgwK_1_1wJcVZQ2LAdSG0Lo,2671
950
956
  django_cfg/modules/django_tailwind/README.md,sha256=tPQd4ir79MLFSVUKBGfPAF22LZWE0BcmzcNdQhJ4k7I,10804
951
957
  django_cfg/modules/django_tailwind/__init__.py,sha256=K0GbFxjemNixRUgexurSTDfN4kB--TmuFCk9n5DbLYc,186
952
958
  django_cfg/modules/django_tailwind/apps.py,sha256=CKNd0xDoLaQpe7SEfay0ZtWkjLUVRgofkB8FobKLXV8,313
@@ -1060,9 +1066,9 @@ django_cfg/utils/version_check.py,sha256=WO51J2m2e-wVqWCRwbultEwu3q1lQasV67Mw2aa
1060
1066
  django_cfg/CHANGELOG.md,sha256=jtT3EprqEJkqSUh7IraP73vQ8PmKUMdRtznQsEnqDZk,2052
1061
1067
  django_cfg/CONTRIBUTING.md,sha256=DU2kyQ6PU0Z24ob7O_OqKWEYHcZmJDgzw-lQCmu6uBg,3041
1062
1068
  django_cfg/LICENSE,sha256=xHuytiUkSZCRG3N11nk1X6q1_EGQtv6aL5O9cqNRhKE,1071
1063
- django_cfg/pyproject.toml,sha256=MwMh3DgYM1djCIx9K6E9rCngbKw5nVTDLWGmYMH6Ueo,8665
1064
- django_cfg-1.4.117.dist-info/METADATA,sha256=Ll9z9f22ytdtCrn-anLFdvYpvVLBer0V7pqxnuefcFI,23876
1065
- django_cfg-1.4.117.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
1066
- django_cfg-1.4.117.dist-info/entry_points.txt,sha256=Ucmde4Z2wEzgb4AggxxZ0zaYDb9HpyE5blM3uJ0_VNg,56
1067
- django_cfg-1.4.117.dist-info/licenses/LICENSE,sha256=xHuytiUkSZCRG3N11nk1X6q1_EGQtv6aL5O9cqNRhKE,1071
1068
- django_cfg-1.4.117.dist-info/RECORD,,
1069
+ django_cfg/pyproject.toml,sha256=fZ9FGyvoUZbCwaNqWJDQwUNYeN11UYgds7UsrzwGFO0,8665
1070
+ django_cfg-1.4.118.dist-info/METADATA,sha256=NcD294dwA8ckrblk-0er6UcqtHdYYren4RsZh77BqkU,23876
1071
+ django_cfg-1.4.118.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
1072
+ django_cfg-1.4.118.dist-info/entry_points.txt,sha256=Ucmde4Z2wEzgb4AggxxZ0zaYDb9HpyE5blM3uJ0_VNg,56
1073
+ django_cfg-1.4.118.dist-info/licenses/LICENSE,sha256=xHuytiUkSZCRG3N11nk1X6q1_EGQtv6aL5O9cqNRhKE,1071
1074
+ django_cfg-1.4.118.dist-info/RECORD,,