python3-commons 0.6.20__py3-none-any.whl → 0.7.1__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.
python3_commons/conf.py CHANGED
@@ -1,4 +1,4 @@
1
- from pydantic import SecretStr
1
+ from pydantic import SecretStr, PostgresDsn
2
2
  from pydantic_settings import BaseSettings
3
3
 
4
4
 
@@ -8,6 +8,10 @@ class CommonSettings(BaseSettings):
8
8
  logging_formatter: str = 'default'
9
9
 
10
10
 
11
+ class DBSettings(BaseSettings):
12
+ db_dsn: PostgresDsn | None = None
13
+
14
+
11
15
  class S3Settings(BaseSettings):
12
16
  s3_endpoint_url: str | None = None
13
17
  s3_region_name: str | None = None
@@ -20,4 +24,5 @@ class S3Settings(BaseSettings):
20
24
 
21
25
 
22
26
  settings = CommonSettings()
27
+ db_settings = DBSettings()
23
28
  s3_settings = S3Settings()
@@ -0,0 +1,60 @@
1
+ import asyncio
2
+ import contextlib
3
+ import logging
4
+ from typing import AsyncGenerator
5
+
6
+ from asyncpg import CannotConnectNowError
7
+ from pydantic import PostgresDsn
8
+ from sqlalchemy import MetaData
9
+ from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
10
+ from sqlalchemy.ext.asyncio.session import async_sessionmaker
11
+ from sqlalchemy.orm import declarative_base
12
+
13
+ from python3_commons.conf import db_settings
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ metadata = MetaData()
18
+ Base = declarative_base(metadata=metadata)
19
+ engine = create_async_engine(
20
+ str(db_settings.db_dsn),
21
+ # echo=True,
22
+ pool_size=20,
23
+ max_overflow=0,
24
+ pool_timeout=30,
25
+ pool_recycle=1800, # 30 minutes
26
+ )
27
+ async_session_maker = async_sessionmaker(engine, expire_on_commit=False)
28
+
29
+
30
+ async def get_async_session() -> AsyncGenerator[AsyncSession, None]:
31
+ async with async_session_maker() as session:
32
+ yield session
33
+
34
+
35
+ get_async_session_context = contextlib.asynccontextmanager(get_async_session)
36
+
37
+
38
+ async def is_healthy(pg) -> bool:
39
+ return await pg.fetchval('SELECT 1 FROM alembic_version;') == 1
40
+
41
+
42
+ async def connect_to_db(database, dsn: PostgresDsn):
43
+ logger.info('Waiting for services')
44
+ logger.debug(f'DB_DSN: {dsn}')
45
+ timeout = 0.001
46
+ total_timeout = 0
47
+
48
+ for i in range(15):
49
+ try:
50
+ await database.connect()
51
+ except (ConnectionRefusedError, CannotConnectNowError):
52
+ timeout *= 2
53
+ await asyncio.sleep(timeout)
54
+ total_timeout += timeout
55
+ else:
56
+ break
57
+ else:
58
+ msg = f'Unable to connect database for {int(total_timeout)}s'
59
+ logger.error(msg)
60
+ raise ConnectionRefusedError(msg)
@@ -0,0 +1,61 @@
1
+ import logging
2
+ from typing import Mapping
3
+
4
+ import sqlalchemy as sa
5
+ from sqlalchemy import desc, asc, func
6
+ from sqlalchemy.sql.elements import BooleanClauseList, UnaryExpression
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ def get_query(search: Mapping[str, str] | None = None,
12
+ order_by: str | None = None,
13
+ columns: Mapping | None = None) -> tuple[BooleanClauseList, UnaryExpression]:
14
+ """
15
+ :columns:
16
+ Param name ->
17
+ 0: Model column
18
+ 1: case-insensitive if True
19
+ 2: cast value to type
20
+ 3: exact match if True, LIKE %value% if False
21
+ """
22
+
23
+ if order_by:
24
+ if order_by.startswith('-'):
25
+ direction = desc
26
+ order_by = order_by[1:]
27
+ else:
28
+ direction = asc
29
+
30
+ order_by_clause = direction(columns[order_by][0])
31
+ else:
32
+ order_by_clause = None
33
+
34
+ if search:
35
+ where_parts = [
36
+ *(
37
+ (func.upper(columns[k][0])
38
+ if columns[k][1]
39
+ else columns[k][0]
40
+ ) == columns[k][2](v)
41
+ for k, v in search.items()
42
+ if columns[k][3]
43
+ ),
44
+ *(
45
+ (func.upper(columns[k][0])
46
+ if columns[k][1]
47
+ else columns[k][0]
48
+ ).like(f'%{v.upper()}%')
49
+ for k, v in search.items()
50
+ if not columns[k][3]
51
+ )
52
+ ]
53
+ else:
54
+ where_parts = None
55
+
56
+ if where_parts:
57
+ where_clause = sa.and_(*where_parts)
58
+ else:
59
+ where_clause = None
60
+
61
+ return where_clause, order_by_clause
@@ -0,0 +1,4 @@
1
+ from python3_commons.db.models.auth import ApiKey, User, UserGroup
2
+ from python3_commons.db.models.rbac import (
3
+ RBACRole, RBACPermission, RBACRolePermission, RBACUserRole, RBACApiKeyRole
4
+ )
@@ -0,0 +1,43 @@
1
+ import uuid
2
+
3
+ from fastapi_users_db_sqlalchemy import GUID, SQLAlchemyBaseUserTableUUID
4
+ from pydantic import AwareDatetime
5
+ from sqlalchemy import (
6
+ String, BIGINT, ForeignKey, DateTime
7
+ )
8
+ from sqlalchemy.orm import Mapped, mapped_column
9
+
10
+ from python3_commons.db import Base
11
+ from python3_commons.db.models.common import BaseDBModel, BaseDBUUIDModel
12
+
13
+
14
+ class UserGroup(BaseDBModel, Base):
15
+ __tablename__ = 'user_groups'
16
+
17
+ name: Mapped[str] = mapped_column(String, nullable=False)
18
+
19
+
20
+ class User(SQLAlchemyBaseUserTableUUID, Base):
21
+ __tablename__ = 'users'
22
+
23
+ username: Mapped[str] = mapped_column(String, unique=True, index=True, nullable=False)
24
+ group_id: Mapped[int | None] = mapped_column(BIGINT, ForeignKey('user_groups.id'))
25
+ role_id: Mapped[uuid.UUID | None] = mapped_column(
26
+ GUID,
27
+ ForeignKey('permissions.id', name='fk_api_key_permission_api_key', ondelete='RESTRICT'),
28
+ nullable=False,
29
+ index=True,
30
+ )
31
+
32
+
33
+ class ApiKey(BaseDBUUIDModel, Base):
34
+ __tablename__ = 'api_keys'
35
+
36
+ user_id: Mapped[uuid.UUID | None] = mapped_column(
37
+ GUID,
38
+ ForeignKey('users.id', name='fk_api_key_user', ondelete='RESTRICT'),
39
+ index=True,
40
+ )
41
+ partner_name: Mapped[str] = mapped_column(String, unique=True)
42
+ key: Mapped[str] = mapped_column(String, unique=True)
43
+ expires_at: Mapped[AwareDatetime] = mapped_column(DateTime(timezone=True))
@@ -0,0 +1,37 @@
1
+ from pydantic import AwareDatetime
2
+ from sqlalchemy import (
3
+ DateTime, BIGINT
4
+ )
5
+ from sqlalchemy.dialects.postgresql import UUID
6
+ from sqlalchemy.ext.compiler import compiles
7
+ from sqlalchemy.orm import Mapped, mapped_column
8
+ from sqlalchemy.sql import expression
9
+ from sqlalchemy.sql.ddl import CreateColumn
10
+
11
+
12
+ class UTCNow(expression.FunctionElement):
13
+ type = DateTime(timezone=True)
14
+
15
+
16
+ @compiles(UTCNow, 'postgresql')
17
+ def pg_utcnow(element, compiler, **kw):
18
+ return "TIMEZONE('utc', CURRENT_TIMESTAMP)"
19
+
20
+
21
+ @compiles(CreateColumn, 'postgresql')
22
+ def use_identity(element, compiler, **kw):
23
+ result = compiler.visit_create_column(element, **kw).replace('SERIAL', 'INT GENERATED BY DEFAULT AS IDENTITY')
24
+
25
+ return result.replace('BIGSERIAL', 'BIGINT GENERATED BY DEFAULT AS IDENTITY')
26
+
27
+
28
+ class BaseDBModel:
29
+ id: Mapped[int] = mapped_column(BIGINT, primary_key=True)
30
+ created_at: Mapped[AwareDatetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=UTCNow())
31
+ updated_at: Mapped[AwareDatetime] = mapped_column(DateTime(timezone=True), onupdate=UTCNow())
32
+
33
+
34
+ class BaseDBUUIDModel:
35
+ uid: Mapped[UUID] = mapped_column(UUID, primary_key=True)
36
+ created_at: Mapped[AwareDatetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=UTCNow())
37
+ updated_at: Mapped[AwareDatetime | None] = mapped_column(DateTime(timezone=True), onupdate=UTCNow())
@@ -0,0 +1,101 @@
1
+ import uuid
2
+
3
+ from fastapi_users_db_sqlalchemy import GUID
4
+ from pydantic import AwareDatetime
5
+ from sqlalchemy import (
6
+ String, DateTime, ForeignKey, PrimaryKeyConstraint, CheckConstraint
7
+ )
8
+ from sqlalchemy.dialects.postgresql import UUID
9
+ from sqlalchemy.orm import Mapped, mapped_column
10
+
11
+ from python3_commons.db import Base
12
+
13
+
14
+ class RBACRole(Base):
15
+ __tablename__ = 'rbac_roles'
16
+
17
+ uid: Mapped[uuid.UUID] = mapped_column(UUID, primary_key=True)
18
+ name: Mapped[str] = mapped_column(String, unique=True, nullable=False)
19
+
20
+
21
+ class RBACPermission(Base):
22
+ __tablename__ = 'rbac_permissions'
23
+
24
+ uid: Mapped[uuid.UUID] = mapped_column(UUID, primary_key=True)
25
+ name: Mapped[str] = mapped_column(String, unique=True, nullable=False)
26
+
27
+ __table_args__ = (
28
+ CheckConstraint("name ~ '^[a-z0-9_.]+$'", name='check_rbac_permissions_name'),
29
+ )
30
+
31
+
32
+ class RBACRolePermission(Base):
33
+ __tablename__ = 'rbac_role_permissions'
34
+
35
+ role_uid: Mapped[uuid.UUID | None] = mapped_column(
36
+ UUID,
37
+ ForeignKey('rbac_roles.uid', name='fk_rbac_role_permissions_role', ondelete='CASCADE'),
38
+ index=True,
39
+ )
40
+ permission_uid: Mapped[uuid.UUID | None] = mapped_column(
41
+ UUID,
42
+ ForeignKey('rbac_permissions.uid', name='fk_rbac_role_permissions_permission', ondelete='CASCADE'),
43
+ index=True,
44
+ )
45
+
46
+ __table_args__ = (
47
+ PrimaryKeyConstraint('role_uid', 'permission_uid', name='pk_rbac_role_permissions'),
48
+ )
49
+
50
+
51
+ class RBACUserRole(Base):
52
+ __tablename__ = 'rbac_user_roles'
53
+
54
+ user_id: Mapped[uuid.UUID | None] = mapped_column(
55
+ GUID,
56
+ ForeignKey('users.id', name='fk_rbac_user_roles_user', ondelete='CASCADE'),
57
+ index=True,
58
+ )
59
+ role_uid: Mapped[uuid.UUID | None] = mapped_column(
60
+ UUID,
61
+ ForeignKey('rbac_roles.uid', name='fk_rbac_user_roles_role', ondelete='CASCADE'),
62
+ index=True,
63
+ )
64
+ starts_at: Mapped[AwareDatetime] = mapped_column(DateTime(timezone=True), nullable=False)
65
+ expires_at: Mapped[AwareDatetime | None] = mapped_column(DateTime(timezone=True))
66
+
67
+ __table_args__ = (
68
+ PrimaryKeyConstraint('user_id', 'role_uid', name='pk_rbac_user_roles'),
69
+ )
70
+
71
+
72
+ class RBACApiKeyRole(Base):
73
+ __tablename__ = 'rbac_api_key_roles'
74
+
75
+ api_key_uid: Mapped[uuid.UUID | None] = mapped_column(
76
+ UUID,
77
+ ForeignKey('api_keys.uid', name='fk_rbac_api_key_roles_user', ondelete='CASCADE'),
78
+ index=True,
79
+ )
80
+ role_uid: Mapped[uuid.UUID | None] = mapped_column(
81
+ UUID,
82
+ ForeignKey('rbac_roles.uid', name='fk_rbac_api_key_roles_role', ondelete='CASCADE'),
83
+ index=True,
84
+ )
85
+ starts_at: Mapped[AwareDatetime] = mapped_column(DateTime(timezone=True), nullable=False)
86
+ expires_at: Mapped[AwareDatetime | None] = mapped_column(DateTime(timezone=True))
87
+
88
+ __table_args__ = (
89
+ PrimaryKeyConstraint('api_key_uid', 'role_uid', name='pk_rbac_api_key_roles'),
90
+ )
91
+
92
+
93
+ # class RBACRoleRelation(Base):
94
+ # __tablename__ = 'rbac_role_relations'
95
+ #
96
+ # parent_uid: Mapped[uuid.UUID] = mapped_column(UUID)
97
+ # child_uid: Mapped[uuid.UUID] = mapped_column(UUID)
98
+ #
99
+ # __table_args__ = (
100
+ # PrimaryKeyConstraint('parent_uid', 'child_uid', name='pk_rbac_role_relations'),
101
+ # )
@@ -0,0 +1,29 @@
1
+ import logging
2
+ from uuid import UUID
3
+
4
+ import sqlalchemy as sa
5
+ from sqlalchemy import func, exists, and_
6
+ from sqlalchemy.ext.asyncio import AsyncSession
7
+
8
+ from python3_commons.db.models import RBACPermission, RBACApiKeyRole, RBACRolePermission
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ async def has_api_key_permission(session: AsyncSession, api_key_uid: UUID, permission: str) -> bool:
14
+ query = sa.select(
15
+ exists().where(
16
+ and_(
17
+ RBACApiKeyRole.api_key_uid == api_key_uid,
18
+ (RBACApiKeyRole.expires_at.is_(None) | (RBACApiKeyRole.expires_at > func.now())),
19
+ RBACApiKeyRole.role_uid == RBACRolePermission.role_uid,
20
+ RBACRolePermission.permission_uid == RBACPermission.uid,
21
+ RBACPermission.name == permission
22
+ )
23
+ )
24
+ )
25
+
26
+ cursor = await session.execute(query)
27
+ result = cursor.scalar()
28
+
29
+ return result
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: python3-commons
3
- Version: 0.6.20
3
+ Version: 0.7.1
4
4
  Summary: Re-usable Python3 code
5
5
  Author-email: Oleg Korsak <kamikaze.is.waiting.you@gmail.com>
6
6
  License: gpl-3
@@ -14,12 +14,15 @@ License-File: LICENSE
14
14
  License-File: AUTHORS.rst
15
15
  Requires-Dist: aiohttp[speedups]~=3.11.13
16
16
  Requires-Dist: asyncpg~=0.30.0
17
+ Requires-Dist: fastapi-users-db-sqlalchemy~=7.0.0
18
+ Requires-Dist: fastapi-users[sqlalchemy]~=14.0.1
17
19
  Requires-Dist: lxml~=5.3.1
18
20
  Requires-Dist: minio~=7.2.15
19
21
  Requires-Dist: msgpack~=1.1.0
20
22
  Requires-Dist: msgspec~=0.19.0
21
23
  Requires-Dist: pydantic[email]~=2.10.6
22
24
  Requires-Dist: pydantic-settings~=2.8.1
25
+ Requires-Dist: SQLAlchemy[asyncio]~=2.0.38
23
26
  Requires-Dist: zeep~=4.3.1
24
27
  Provides-Extra: testing
25
28
  Requires-Dist: pytest; extra == "testing"
@@ -0,0 +1,27 @@
1
+ python3_commons/__init__.py,sha256=0KgaYU46H_IMKn-BuasoRN3C4Hi45KlkHHoPbU9cwiA,189
2
+ python3_commons/api_client.py,sha256=6F99vSGCV8pqcKJE9dECijlgggm40FB9TzkFZRgwDq4,4419
3
+ python3_commons/audit.py,sha256=DMQ-nrWSs0qilD7wkz_8PV4jXcee75O8FgAm2YIuOiY,6256
4
+ python3_commons/conf.py,sha256=GAcjlFlloLriZXa9JmNGMtTPzvtHUActYDiE4C1DacU,749
5
+ python3_commons/fs.py,sha256=wfLjybXndwLqNlOxTpm_HRJnuTcC4wbrHEOaEeCo9Wc,337
6
+ python3_commons/helpers.py,sha256=OmuCF7UeJ6oe-rH1Y4ZYVW_uPQ973lCLsikj01wmNqs,3101
7
+ python3_commons/object_storage.py,sha256=Bte49twIJ4l6VAzIqK6tLx7DFYwijErUmmhWkrZpSJE,4074
8
+ python3_commons/permissions.py,sha256=5XT5mpz6fSKqm8xBB5OrIdd3YqiCQ-wWuzoc7ZHMnnc,921
9
+ python3_commons/db/__init__.py,sha256=pZk0Fv-Le2sFC-KaVDdJjDDsHtmv-IJz_aFP5o2PGHA,1682
10
+ python3_commons/db/helpers.py,sha256=6xYgDgMhWUpVbtD3FctYtHVBRNahCkkpVIxx0matt-c,1644
11
+ python3_commons/db/models/__init__.py,sha256=AeeTLUqdqQrhCTi14ACWO0ccyVyWFppZVtCeWShSvR0,193
12
+ python3_commons/db/models/auth.py,sha256=q26zvys7lXu4Waai2ZK1YJLdp9eDBxSQ2_8x6DP3YvY,1404
13
+ python3_commons/db/models/common.py,sha256=ScFEL04HwEomX3X7f8WHMq505uvoID6itBETRWfya9w,1402
14
+ python3_commons/db/models/rbac.py,sha256=atgCfAWgb8BJYKd7wuzV1HTDhasCL7448iPRNLBAjlw,3249
15
+ python3_commons/logging/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
+ python3_commons/logging/filters.py,sha256=fuyjXZAUm-i2MNrxvFYag8F8Rr27x8W8MdV3ke6miSs,175
17
+ python3_commons/logging/formatters.py,sha256=UXmmh1yd5Kc2dpvSHn6uCWLDWE2LMjlYAaH8cg3siV4,720
18
+ python3_commons/serializers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
+ python3_commons/serializers/json.py,sha256=P288wWz9ic38QWEMrpp_uwKPYkQiOgvE1cI4WZn6ZCg,808
20
+ python3_commons/serializers/msgpack.py,sha256=tzIGGyDL3UpZnnouCtnxuYDx6InKM_C3PP1N4PN8wd4,1269
21
+ python3_commons/serializers/msgspec.py,sha256=FuZVqOLJb0-lEKrs7dtjhwEbHIpfMUk5yu1hD64zRdc,2038
22
+ python3_commons-0.7.1.dist-info/AUTHORS.rst,sha256=3R9JnfjfjH5RoPWOeqKFJgxVShSSfzQPIrEr1nxIo9Q,90
23
+ python3_commons-0.7.1.dist-info/LICENSE,sha256=xxILuojHm4fKQOrMHPSslbyy6WuKAN2RiG74HbrYfzM,34575
24
+ python3_commons-0.7.1.dist-info/METADATA,sha256=IxBKctb_ZVeIAoNfjl2tdC6LB87dg8EehIqcgKceolg,1127
25
+ python3_commons-0.7.1.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
26
+ python3_commons-0.7.1.dist-info/top_level.txt,sha256=lJI6sCBf68eUHzupCnn2dzG10lH3jJKTWM_hrN1cQ7M,16
27
+ python3_commons-0.7.1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.8.2)
2
+ Generator: setuptools (76.0.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
python3_commons/db.py DELETED
@@ -1,28 +0,0 @@
1
- import asyncio
2
- import logging
3
-
4
- from asyncpg import CannotConnectNowError
5
- from pydantic import PostgresDsn
6
-
7
- logger = logging.getLogger(__name__)
8
-
9
-
10
- async def connect_to_db(database, dsn: PostgresDsn):
11
- logger.info('Waiting for services')
12
- logger.debug(f'DB_DSN: {dsn}')
13
- timeout = 0.001
14
- total_timeout = 0
15
-
16
- for i in range(15):
17
- try:
18
- await database.connect()
19
- except (ConnectionRefusedError, CannotConnectNowError):
20
- timeout *= 2
21
- await asyncio.sleep(timeout)
22
- total_timeout += timeout
23
- else:
24
- break
25
- else:
26
- msg = f'Unable to connect database for {int(total_timeout)}s'
27
- logger.error(msg)
28
- raise ConnectionRefusedError(msg)
@@ -1,21 +0,0 @@
1
- python3_commons/__init__.py,sha256=0KgaYU46H_IMKn-BuasoRN3C4Hi45KlkHHoPbU9cwiA,189
2
- python3_commons/api_client.py,sha256=6F99vSGCV8pqcKJE9dECijlgggm40FB9TzkFZRgwDq4,4419
3
- python3_commons/audit.py,sha256=DMQ-nrWSs0qilD7wkz_8PV4jXcee75O8FgAm2YIuOiY,6256
4
- python3_commons/conf.py,sha256=qm2a2yWOhfawicBPjWnUett8TrsMtoyQXDxEJ_N-v-Y,637
5
- python3_commons/db.py,sha256=qhaDIdzBWgFyeP_XPKfHZlYVlwS2bpBPYMv84yV6820,738
6
- python3_commons/fs.py,sha256=wfLjybXndwLqNlOxTpm_HRJnuTcC4wbrHEOaEeCo9Wc,337
7
- python3_commons/helpers.py,sha256=OmuCF7UeJ6oe-rH1Y4ZYVW_uPQ973lCLsikj01wmNqs,3101
8
- python3_commons/object_storage.py,sha256=Bte49twIJ4l6VAzIqK6tLx7DFYwijErUmmhWkrZpSJE,4074
9
- python3_commons/logging/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
- python3_commons/logging/filters.py,sha256=fuyjXZAUm-i2MNrxvFYag8F8Rr27x8W8MdV3ke6miSs,175
11
- python3_commons/logging/formatters.py,sha256=UXmmh1yd5Kc2dpvSHn6uCWLDWE2LMjlYAaH8cg3siV4,720
12
- python3_commons/serializers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
- python3_commons/serializers/json.py,sha256=P288wWz9ic38QWEMrpp_uwKPYkQiOgvE1cI4WZn6ZCg,808
14
- python3_commons/serializers/msgpack.py,sha256=tzIGGyDL3UpZnnouCtnxuYDx6InKM_C3PP1N4PN8wd4,1269
15
- python3_commons/serializers/msgspec.py,sha256=FuZVqOLJb0-lEKrs7dtjhwEbHIpfMUk5yu1hD64zRdc,2038
16
- python3_commons-0.6.20.dist-info/AUTHORS.rst,sha256=3R9JnfjfjH5RoPWOeqKFJgxVShSSfzQPIrEr1nxIo9Q,90
17
- python3_commons-0.6.20.dist-info/LICENSE,sha256=xxILuojHm4fKQOrMHPSslbyy6WuKAN2RiG74HbrYfzM,34575
18
- python3_commons-0.6.20.dist-info/METADATA,sha256=k4xVOGoFKRMpICx37j9DZ5Wzp1cNpTBUapsmhRZ8w1Q,986
19
- python3_commons-0.6.20.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
20
- python3_commons-0.6.20.dist-info/top_level.txt,sha256=lJI6sCBf68eUHzupCnn2dzG10lH3jJKTWM_hrN1cQ7M,16
21
- python3_commons-0.6.20.dist-info/RECORD,,