utg-base 1.3.2__py3-none-any.whl → 1.5.0__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.
@@ -0,0 +1,11 @@
1
+ from rest_framework.permissions import BasePermission
2
+
3
+
4
+ class IsSuperUser(BasePermission):
5
+ """
6
+ Custom permission to allow access only to superusers.
7
+ """
8
+
9
+ def has_permission(self, request, view):
10
+ # Check if the requesting user is a superuser
11
+ return request.user and request.user.is_superuser
File without changes
@@ -0,0 +1,6 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class QApiConfig(AppConfig):
5
+ default_auto_field = "django.db.models.BigAutoField"
6
+ name = "utg_base.celery"
@@ -0,0 +1 @@
1
+ from .task_result import TaskResultFilterSet
@@ -0,0 +1,5 @@
1
+ from django_filters import rest_framework as filters
2
+
3
+
4
+ class TaskResultFilterSet(filters.FilterSet):
5
+ status = filters.BooleanFilter()
File without changes
File without changes
@@ -0,0 +1,45 @@
1
+ import importlib
2
+ import json
3
+
4
+ from django.core.management.base import BaseCommand
5
+ from django_celery_beat.models import CrontabSchedule, PeriodicTask
6
+
7
+
8
+ class Command(BaseCommand):
9
+ help = "Migrate tasks"
10
+
11
+ def add_arguments(self, parser):
12
+ parser.add_argument('--app', type=str, help="An optional argument", required=True)
13
+
14
+ def handle(self, *args, **options):
15
+ try:
16
+ tasks = importlib.import_module(f"{options['app']}.tasks")
17
+ tasks = tasks.tasks
18
+ for task in tasks:
19
+ celery_task, _ = PeriodicTask.objects.update_or_create(
20
+ name=task['name'],
21
+ defaults={
22
+ 'crontab': self.get_crontab_schedule(task['crontab']),
23
+ 'task': task['task'].name,
24
+ 'args': json.dumps(task.get('args') or []),
25
+ 'kwargs': json.dumps(task.get('kwargs') or {}),
26
+ 'enabled': task.get('enabled', True),
27
+ }
28
+ )
29
+ self.stdout.write(
30
+ self.style.SUCCESS('Successfully migrated tasks')
31
+ )
32
+ except ModuleNotFoundError:
33
+ self.stdout.write(self.style.ERROR("Tasks module not found"))
34
+
35
+ @staticmethod
36
+ def get_crontab_schedule(crontab='* * * * *'):
37
+ minute, hour, day_of_month, month_of_year, day_of_week = crontab.split(' ')
38
+ cron, _ = CrontabSchedule.objects.get_or_create(
39
+ minute=minute,
40
+ hour=hour,
41
+ day_of_month=day_of_month,
42
+ month_of_year=month_of_year,
43
+ day_of_week=day_of_week,
44
+ )
45
+ return cron
@@ -0,0 +1,2 @@
1
+ from .task_result import TaskResultSerializer
2
+ from .periodic_task import PeriodicTaskSerializer
@@ -0,0 +1,38 @@
1
+ from django_celery_beat.models import PeriodicTask, CrontabSchedule
2
+ from django_celery_results.models import TaskResult
3
+ from rest_framework import serializers
4
+
5
+
6
+ from utg_base.celery.serializers import TaskResultSerializer
7
+
8
+
9
+ class CrontabScheduleSerializer(serializers.ModelSerializer):
10
+ timezone = serializers.SerializerMethodField()
11
+
12
+ class Meta:
13
+ model = CrontabSchedule
14
+ fields = '__all__'
15
+
16
+ def get_timezone(self, obj):
17
+ return str(obj.timezone) if obj.timezone else None
18
+
19
+
20
+ class PeriodicTaskSerializer(serializers.ModelSerializer):
21
+ crontab = CrontabScheduleSerializer(read_only=True)
22
+ task_result = serializers.SerializerMethodField(read_only=True)
23
+
24
+ class Meta:
25
+ model = PeriodicTask
26
+ fields = '__all__'
27
+ read_only_fields = ['name', 'task', 'args', 'kwargs', 'queue', 'exchange', 'routing_key', 'headers', 'priority',
28
+ 'expires', 'expire_seconds', 'one_off', 'start_time', 'interval', 'solar', 'clocked']
29
+
30
+ def get_task_result(self, obj):
31
+ latest_task_result = TaskResult.objects.filter(
32
+ periodic_task_name=obj.name
33
+ ).order_by('-date_created').first()
34
+
35
+ if not latest_task_result:
36
+ return None
37
+
38
+ return TaskResultSerializer(latest_task_result).data
@@ -0,0 +1,8 @@
1
+ from django_celery_results.models import TaskResult
2
+ from rest_framework import serializers
3
+
4
+
5
+ class TaskResultSerializer(serializers.ModelSerializer):
6
+ class Meta:
7
+ model = TaskResult
8
+ fields = '__all__'
@@ -0,0 +1,14 @@
1
+ from django.urls import path
2
+
3
+ from utg_base.api.routers import OptionalSlashRouter
4
+ from utg_base.celery.views import TaskResultViewSet, PeriodicTaskViewSet, PeriodicTaskRunNowView
5
+
6
+ router = OptionalSlashRouter()
7
+ router.register('task-results', TaskResultViewSet, basename='task-results')
8
+ router.register('periodic-tasks', PeriodicTaskViewSet, basename='periodic-tasks')
9
+
10
+ urlpatterns = [
11
+ path('periodic-tasks/<pk>/run-now/', PeriodicTaskRunNowView.as_view(), name='periodic-tasks-run-now'),
12
+ ]
13
+
14
+ urlpatterns += router.urls
@@ -0,0 +1,2 @@
1
+ from .periodic_task import PeriodicTaskViewSet, PeriodicTaskRunNowView
2
+ from .task_result import TaskResultViewSet
@@ -0,0 +1,56 @@
1
+ import json
2
+
3
+ from celery import current_app
4
+ from django_celery_beat.models import PeriodicTask
5
+ from drf_spectacular.utils import extend_schema
6
+ from rest_framework import viewsets, filters
7
+ from rest_framework.exceptions import NotFound, ValidationError
8
+ from rest_framework.permissions import IsAuthenticated
9
+ from rest_framework.response import Response
10
+ from rest_framework.views import APIView
11
+
12
+ from utg_base.api.permissions import IsSuperUser
13
+ from utg_base.celery.serializers import PeriodicTaskSerializer
14
+ from utg_base.utils.translation import translate as _
15
+
16
+
17
+ @extend_schema(tags=['admin/periodic-tasks'])
18
+ class PeriodicTaskViewSet(viewsets.ModelViewSet):
19
+ http_method_names = ['get', 'patch']
20
+ queryset = PeriodicTask.objects.all()
21
+ serializer_class = PeriodicTaskSerializer
22
+ permission_classes = [IsSuperUser]
23
+ filter_backends = [filters.SearchFilter]
24
+ search_fields = ['name']
25
+
26
+
27
+ @extend_schema(tags=['admin/periodic-tasks'])
28
+ class PeriodicTaskRunNowView(APIView):
29
+ permission_classes = [IsAuthenticated]
30
+
31
+ def patch(self, request, pk):
32
+ try:
33
+ periodic_task = PeriodicTask.objects.get(pk=pk)
34
+ except PeriodicTask.DoesNotExist:
35
+ raise NotFound(detail=_("Periodic task not found"))
36
+
37
+ if not periodic_task.enabled:
38
+ raise ValidationError(detail=_("Task is disabled"))
39
+
40
+ args = json.loads(periodic_task.args) if periodic_task.args else []
41
+ kwargs = json.loads(periodic_task.kwargs) if periodic_task.kwargs else {}
42
+
43
+ task = current_app.send_task(
44
+ periodic_task.task,
45
+ args=args,
46
+ kwargs=kwargs,
47
+ countdown=0,
48
+ headers = {'periodic_task_name': periodic_task.name}
49
+ )
50
+
51
+ return Response({
52
+ "detail": _("Task successfully triggered"),
53
+ "task_id": task.id,
54
+ "task_name": periodic_task.name,
55
+ "celery_task": periodic_task.task,
56
+ })
@@ -0,0 +1,18 @@
1
+ import django_filters
2
+ from django_celery_results.models import TaskResult
3
+ from drf_spectacular.utils import extend_schema
4
+ from rest_framework import viewsets, filters
5
+
6
+ from utg_base.celery.filters import TaskResultFilterSet
7
+ from utg_base.api.permissions import IsSuperUser
8
+ from utg_base.celery.serializers import TaskResultSerializer
9
+
10
+
11
+ @extend_schema(tags=['admin/task-results'])
12
+ class TaskResultViewSet(viewsets.ReadOnlyModelViewSet):
13
+ queryset = TaskResult.objects.all()
14
+ serializer_class = TaskResultSerializer
15
+ permission_classes = [IsSuperUser]
16
+ filterset_class = TaskResultFilterSet
17
+ filter_backends = [filters.SearchFilter, django_filters.rest_framework.DjangoFilterBackend]
18
+ search_fields = ['name']
utg_base/env.py ADDED
@@ -0,0 +1,27 @@
1
+ import os
2
+
3
+ import dotenv
4
+ import hvac
5
+ from django.conf import settings
6
+
7
+ dotenv.load_dotenv(settings.BASE_DIR / '.env')
8
+
9
+ client = hvac.Client(
10
+ url=os.environ.get("VAULT_URL"),
11
+ token=os.environ.get("VAULT_TOKEN"),
12
+ verify=False
13
+ )
14
+ if not client.is_authenticated():
15
+ raise Exception("Vault authentication failed")
16
+
17
+ envs = client.secrets.kv.read_secret_version(
18
+ path=os.environ.get("VAULT_PATH"),
19
+ mount_point="utg-scada"
20
+ )['data']['data']
21
+
22
+
23
+ def env(key, default=None):
24
+ if key in os.environ:
25
+ return os.environ.get(key, default)
26
+ else:
27
+ return envs.get(key, default)
utg_base/logging.py CHANGED
@@ -21,7 +21,6 @@ class UtgBaseFilter(logging.Filter):
21
21
 
22
22
 
23
23
  class UtgBaseLogging:
24
- enable_file = int(os.environ.get('LOGGING_FILE', False))
25
24
  enable_console_info = int(os.environ.get('LOGGING_CONSOLE_INFO', True))
26
25
  enable_db_debug = int(os.environ.get('LOGGING_DB_DEBUG', False))
27
26
  enable_loki = int(os.environ.get('LOGGING_LOKI', True))
@@ -51,13 +50,6 @@ class UtgBaseLogging:
51
50
  },
52
51
  },
53
52
  'handlers': {
54
- 'file': {
55
- 'level': 'ERROR',
56
- 'class': 'logging.FileHandler',
57
- 'filename': 'logs/django-error.log',
58
- 'formatter': 'simple',
59
- 'filters': ['trim_path'],
60
- },
61
53
  'console_info': {
62
54
  'level': 'INFO',
63
55
  'class': 'logging.StreamHandler',
@@ -114,8 +106,6 @@ class UtgBaseLogging:
114
106
  def logging(cls):
115
107
  _logging = cls.__LOGGING.copy()
116
108
 
117
- if cls.enable_file:
118
- _logging['loggers']['django']['handlers'].append('file')
119
109
  if cls.enable_console_info:
120
110
  _logging['loggers']['django']['handlers'].append('console_info')
121
111
  if cls.enable_db_debug:
@@ -1 +1,2 @@
1
1
  from .jwt_user import JWTUser
2
+ from .timestamp import BaseTimestamp
@@ -0,0 +1,9 @@
1
+ from django.db import models
2
+
3
+
4
+ class BaseTimestamp(models.Model):
5
+ created_at = models.DateTimeField(auto_now_add=True)
6
+ updated_at = models.DateTimeField(auto_now=True)
7
+
8
+ class Meta:
9
+ abstract = True
utg_base/utils/data.py CHANGED
@@ -24,13 +24,24 @@ def deep_map(data: dict | list, func_cond, func_map, in_place=True):
24
24
  return data
25
25
 
26
26
 
27
- def deep_round(data: dict | list, ndigits: int, in_place=True):
28
- return deep_map(data, lambda value: isinstance(value, float), lambda value: round(value, ndigits), in_place)
27
+ def to_numeric_or_none(*args):
28
+ _is_iterable = isinstance(args[0], Iterable) and not isinstance(args[0], str)
29
+ if _is_iterable:
30
+ args = args[0]
31
+ result = []
32
+
33
+ for x in args:
34
+ if isinstance(x, (int, float, complex, bool)):
35
+ result.append(x)
36
+ else:
37
+ result.append(None)
38
+ if not _is_iterable and len(result) == 1 and len(args) == 1:
39
+ return result[0]
40
+ return result
29
41
 
30
42
 
31
43
  def safe_sum(*args, allow_null=True):
32
- if isinstance(args[0], Iterable):
33
- args = args[0]
44
+ args = to_numeric_or_none(*args)
34
45
 
35
46
  if all(arg is None for arg in args):
36
47
  return None
@@ -44,8 +55,7 @@ def safe_sum(*args, allow_null=True):
44
55
 
45
56
 
46
57
  def safe_subtract(*args, allow_null=False):
47
- if isinstance(args[0], Iterable):
48
- args = args[0]
58
+ args = to_numeric_or_none(*args)
49
59
 
50
60
  if all(arg is None for arg in args):
51
61
  return None
@@ -56,3 +66,37 @@ def safe_subtract(*args, allow_null=False):
56
66
  for arg in args[1:]:
57
67
  _sum -= arg or 0
58
68
  return _sum
69
+
70
+
71
+ def safe_multiply(*args, allow_null=False):
72
+ args = to_numeric_or_none(*args)
73
+
74
+ if all(arg is None for arg in args):
75
+ return None
76
+ if not allow_null and any(arg is None for arg in args):
77
+ return None
78
+
79
+ result = 1
80
+ for arg in args:
81
+ value = 0 if allow_null and arg is None else arg
82
+ result *= value
83
+ return result
84
+
85
+
86
+ def safe_divide(*args, allow_null=False):
87
+ args = to_numeric_or_none(*args)
88
+
89
+ if all(arg is None for arg in args):
90
+ return None
91
+ if not allow_null and any(arg is None for arg in args):
92
+ return None
93
+
94
+ result = 0 if allow_null and args[0] is None else args[0]
95
+
96
+ for arg in args[1:]:
97
+ value = 0 if allow_null and arg is None else arg
98
+ if value == 0:
99
+ return None
100
+ result /= value
101
+
102
+ return result
@@ -1,17 +1,22 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: utg-base
3
- Version: 1.3.2
3
+ Version: 1.5.0
4
4
  Summary: UTG Base Package
5
5
  Author: Rovshen
6
6
  Author-email: rovshenashirov1619@gmail.com
7
7
  Requires-Python: >=3.14,<4.0
8
8
  Classifier: Programming Language :: Python :: 3
9
+ Requires-Dist: celery (>=5.5.3,<6.0.0)
9
10
  Requires-Dist: croniter (>=2.0.3,<3.0.0)
10
11
  Requires-Dist: django (>=5.2.7,<6.0.0)
12
+ Requires-Dist: django-celery-beat (>=2.8.1,<3.0.0)
13
+ Requires-Dist: django-celery-results (>=2.6.0,<3.0.0)
11
14
  Requires-Dist: django-filter (>=23.5,<24.0)
12
15
  Requires-Dist: djangorestframework (>=3.16.1,<4.0.0)
13
16
  Requires-Dist: djangorestframework-simplejwt[crypto] (>=5.5.1,<6.0.0)
17
+ Requires-Dist: dotenv (>=0.9.9,<0.10.0)
14
18
  Requires-Dist: drf-spectacular[sidecar] (>=0.29.0,<0.30.0)
19
+ Requires-Dist: hvac (>=2.4.0,<3.0.0)
15
20
  Requires-Dist: inflect (>=7.2.1,<8.0.0)
16
21
  Requires-Dist: openpyxl (>=3.1.2,<4.0.0)
17
22
  Description-Content-Type: text/markdown
@@ -2,19 +2,36 @@ utg_base/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  utg_base/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  utg_base/api/base.py,sha256=giGKC68sL1j1NQbbkhIWzhHPcPlvjTPG2evra-xAJ3s,2574
4
4
  utg_base/api/pagination.py,sha256=zzIUwW3iF5G_11gFsno9y1DmgFiULQIWRHUj0LKhYfE,854
5
+ utg_base/api/permissions.py,sha256=RjkxYWPl5Xwgk5lIZrcBzIAwlXolX5XPlae1jr-Ln2w,323
5
6
  utg_base/api/routers.py,sha256=lU54MVN2BF_q1AWp9EdXkG3m_ivYRtvbNGXFIRKz7u0,177
6
7
  utg_base/api/serializers.py,sha256=qI6wWjwl1oeUPHCJUCpYFIaiRFvfQW6FM0xPC9fwfQI,1214
7
8
  utg_base/api/views.py,sha256=yYCEJRouFA71cI2Ubc1A736oLg9NGWyTIVnD-Q85k6w,279
8
9
  utg_base/authentications/__init__.py,sha256=a6twO_bBf8FAHYl7PXawfR2UbBwwdueG1uS_dbq2g_I,109
9
10
  utg_base/authentications/microservice_authentication.py,sha256=6aAncxIpA4FcyRegd7QqRYvW5Wn8FxyPU0nQqCVuEs4,976
10
11
  utg_base/authentications/models.py,sha256=JQonSdXeSeoF003QlmPvH58nWmVJRKlWWjW_ySqXaYg,2496
12
+ utg_base/celery/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
+ utg_base/celery/apps.py,sha256=eL9Il9gGLrtZtXOxWgQK1j8JH5g677h_He7Z1ldUiuA,151
14
+ utg_base/celery/filters/__init__.py,sha256=wnkV8cPEvvHIl8OoMTv7fvSglDSFY8XxpCjUveRNUyg,45
15
+ utg_base/celery/filters/task_result.py,sha256=iXgOrXrO8bvF9S47D53UzsdWQr_JrJ-LtxdjPfcTVjA,138
16
+ utg_base/celery/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
+ utg_base/celery/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
+ utg_base/celery/management/commands/migrate_tasks.py,sha256=TAvPmYAmMkODdPAucbQDya2dd-BkVFCvWJrolqFdz6w,1667
19
+ utg_base/celery/serializers/__init__.py,sha256=NjQ4dcnyoX9p5ljUBCjDegTKt7icia48GkVSmFCK9Ao,96
20
+ utg_base/celery/serializers/periodic_task.py,sha256=lQShBqUQUOvk6-VQocyGfATZHAAVrng-w6_VjD3_WH4,1298
21
+ utg_base/celery/serializers/task_result.py,sha256=pt6BRjjvqU1Ah8IUSyL67AZJ1Z5iXAelYe6z-Pv1Nco,220
22
+ utg_base/celery/urls.py,sha256=ww-ZwAzK0CnOZxqh4gaEYGuRoCgkrolB3jvKsLZEknI,524
23
+ utg_base/celery/views/__init__.py,sha256=WPNtK_40JCSYGWkDgtpTdOWJ_GOp_dBKs9eK4VNq5Xs,114
24
+ utg_base/celery/views/periodic_task.py,sha256=OyRJh_-KCicUZD84hksRoOXLHGk7mLF7QlsjncqvQac,1945
25
+ utg_base/celery/views/task_result.py,sha256=c9HIcohrToRfz1jfZRRZ1ri15FOasjKgMYnzKcA2X8M,726
11
26
  utg_base/constants/__init__.py,sha256=nC8qE-2V6APtjSz8j0A-3ez8yyoRpdRO8pwQnvvpRMk,53
12
27
  utg_base/constants/available_languages.py,sha256=zQh0S0PMuYUdRW_RH36llvMxbvsfbdUtotDjFeysWfQ,56
13
- utg_base/logging.py,sha256=MNPUQ9Hdg-0YTFlJBOwhajqKz-8Y5P4POkqRNIU9ga8,5078
28
+ utg_base/env.py,sha256=1Ljxt9clniKl_P2FKG9eoU-7lj00iLHKWDcSVd8wBD4,603
29
+ utg_base/logging.py,sha256=6mqhirIz5p1ne3av4S8j02TD5lJ4HyDKvMQsfUJ62po,4667
14
30
  utg_base/middleware/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
31
  utg_base/middleware/locale.py,sha256=1hp_T_VuHCz0ITjwJ_F1rpf5kXQ0ulEmK1yoRWD1GRc,557
16
- utg_base/models/__init__.py,sha256=ylLp_n2WJqvmuG-4X8yv119Ikcx-OFSYHUcnggUeQeo,30
32
+ utg_base/models/__init__.py,sha256=1zXygGICiR3iUCKdkNal9d3i3kNp654gFgBf_VlR2gI,67
17
33
  utg_base/models/jwt_user.py,sha256=6TQ5wB_OZBtGhRL-2MonBGZm0n0Y86s4BRTxiRlUJOk,375
34
+ utg_base/models/timestamp.py,sha256=AkCliNXnvs8Z17b1mcS7gOK7v6h3Jul6WCyGyVAkb-w,217
18
35
  utg_base/references_api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
36
  utg_base/references_api/admin.py,sha256=suMo4x8I3JBxAFBVIdE-5qnqZ6JAZV0FESABHOSc-vg,63
20
37
  utg_base/references_api/apps.py,sha256=thAGmO-ZT-OD9dHHBSQRL_RRt-Es_jt-mEmHgVTpERs,168
@@ -25,12 +42,12 @@ utg_base/references_api/utils.py,sha256=VzQMnGeWC0I-6BkK_9Lo9eSH6wd4C766oQ84n7SB
25
42
  utg_base/services/__init__.py,sha256=LqtwUiqEZPIbKRGJfve5D5m3ucV6Kw1Nbo5Jnj_hPhY,37
26
43
  utg_base/services/base_api.py,sha256=RBwzO6frYs2TeMKkAohUxtH9JhDTgmb9G5BFijajg68,5207
27
44
  utg_base/utils/__init__.py,sha256=5XmIPVpOl9Tjtzkx_bBeZD1uCpBE-R3WX6yiJii9Ip0,101
28
- utg_base/utils/data.py,sha256=XmGJStl2f5Bx2B5F45KOUlyS7JkxYiVdWu66_RA_sWQ,1635
45
+ utg_base/utils/data.py,sha256=luD1L8RwAtvs-qraQMJ2K34fZTlGZy1PvClNeF_Ei-s,2661
29
46
  utg_base/utils/date.py,sha256=thcbK6RgTUYZfs4_vW5ucuu2e8H0rei6tv7SEC72iwM,3612
30
47
  utg_base/utils/dict_util.py,sha256=ipdCZO8aTukGQ319OWHb2Ij5MNtV-FioJQ4qCX3Th48,758
31
48
  utg_base/utils/response_processors.py,sha256=WdZQL49wOJqCIY2MucAI6sez_llCqih0v_ltQa-mv7k,687
32
49
  utg_base/utils/sql.py,sha256=rqIWcSjdjIMszdRnsnhV5TTYB8W17RPOujIQA9rKC_Y,762
33
50
  utg_base/utils/translation.py,sha256=HAUB64h0Maw82ehCoi0Yb6V6gj1Y5l5RMsv8_FMoV2U,456
34
- utg_base-1.3.2.dist-info/METADATA,sha256=l3-bW6rMxuC_4bQtj6PGK-9Rndx2pwQHkTtmypb0-D0,660
35
- utg_base-1.3.2.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
36
- utg_base-1.3.2.dist-info/RECORD,,
51
+ utg_base-1.5.0.dist-info/METADATA,sha256=E-cHB0OHYpNnWjOuSBCW-H5dxE8EtirNZOLsTn3gJXE,881
52
+ utg_base-1.5.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
53
+ utg_base-1.5.0.dist-info/RECORD,,