This commit is contained in:
2025-04-12 21:04:02 -03:00
parent 1b2ebcfb99
commit 86bdb41216
19 changed files with 259 additions and 60 deletions

View File

@@ -13,3 +13,4 @@ if 'PYTEST_VERSION' in os.environ:
DYNAMODB_ENDPOINT_URL = 'http://127.0.0.1:8000'
dynamodb_client = boto3.client('dynamodb', endpoint_url=DYNAMODB_ENDPOINT_URL)
idp_client = boto3.client('cognito-idp')

View File

@@ -57,10 +57,11 @@ class Elastic:
index: str,
doc: dict,
):
return self.client.index(
return self.client.update(
index=index,
id=id,
document=_serialize_to_basic_types(doc),
doc=_serialize_to_basic_types(doc),
doc_as_upsert=True,
)
def delete_index(self, index: str) -> bool:
@@ -90,12 +91,12 @@ if __name__ == '__main__':
# Populate DynamoDB tables with data from JSONL files
for file in tqdm(jsonl_files, desc='Processing files'):
with jsonlines.open(f'seeds/{file}') as lines:
with open(f'seeds/{file}') as fp:
table_name = file.removesuffix('.jsonl')
reader = jsonlines.Reader(fp)
reader = jsonlines.Reader(fp).iter(skip_invalid=True)
for line in tqdm(lines, desc=f'Processing lines in {file}'):
put_item(line, table_name, dynamodb_client)
for line in tqdm(reader, desc=f'Processing lines in {file}'):
put_item(line, table_name, dynamodb_client) # type: ignore
# Scan DynamoDB tables and index the data into Elasticsearch
for file in tqdm(jsonl_files, desc='Scanning tables'):

View File

@@ -1,3 +1,8 @@
from aws_lambda_powertools import Logger
logger = Logger(__name__)
class UnauthorizedError(Exception):
pass
@@ -10,3 +15,24 @@ def get_user(access_token: str, /, idp_client) -> dict[str, str]:
raise UnauthorizedError()
else:
return {attr['Name']: attr['Value'] for attr in user['UserAttributes']}
def admin_get_user(
sub: str,
user_pool_id: str,
*,
idp_client,
) -> dict[str, str] | None:
"""Gets the specified user by user name in a user pool as an administrator.
Works on any user.
- https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/cognito-idp/client/admin_get_user.html
- https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_AdminGetUser.html
"""
try:
user = idp_client.admin_get_user(Username=sub, UserPoolId=user_pool_id)
except idp_client.exceptions as err:
logger.exception(err)
return None
else:
return user

View File

@@ -10,13 +10,13 @@ def create_course(
/,
persistence_layer: DynamoDBPersistenceLayer,
):
current_time = now()
now_ = now()
transact = TransactItems(persistence_layer.table_name)
transact.put(
item={
'sk': '0',
'tenant__org_id': {org.id},
'create_date': current_time,
'create_date': now_,
**course.model_dump(),
}
)
@@ -26,7 +26,7 @@ def create_course(
'sk': 'tenant',
'org_id': org.id,
'name': org.name,
'create_date': current_time,
'create_date': now_,
}
)
return persistence_layer.transact_write_items(transact)
@@ -38,7 +38,7 @@ def update_course(
/,
persistence_layer: DynamoDBPersistenceLayer,
):
current_time = now()
now_ = now()
transact = TransactItems(persistence_layer.table_name)
transact.update(
key=KeyPair(id, '0'),
@@ -50,7 +50,7 @@ def update_course(
':name': course.name,
':cert': course.cert.model_dump() if course.cert else None,
':access_period': course.access_period,
':update_date': current_time,
':update_date': now_,
},
cond_expr='attribute_exists(sk)',
)

View File

@@ -1,15 +1,28 @@
import json
from typing import Literal
from typing import TypedDict
from aws_lambda_powertools.event_handler.api_gateway import Router
from elasticsearch import Elasticsearch
from pydantic import BaseModel
from layercake.dynamodb import (
DynamoDBCollection,
DynamoDBPersistenceLayer,
SortKey,
TransactKey,
)
from pydantic import UUID4, BaseModel
import elastic
from settings import ELASTIC_CONN, ENROLLMENT_TABLE
from boto3clients import dynamodb_client
from middlewares.audit_log_middleware import AuditLogMiddleware
from middlewares.authorizer_middleware import User
from settings import ELASTIC_CONN, ENROLLMENT_TABLE, USER_TABLE
router = Router()
elastic_client = Elasticsearch(**ELASTIC_CONN)
enrollment_layer = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client)
user_layer = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client)
enrollment_collect = DynamoDBCollection(enrollment_layer)
user_collect = DynamoDBCollection(user_layer)
@router.get('/', compress=True, tags=['Enrollment'])
@@ -28,16 +41,44 @@ def get_enrollments():
@router.get('/<id>', compress=True, tags=['Enrollment'])
def get_enrollment(id: str):
return {}
return enrollment_collect.get_items(
TransactKey(id)
+ SortKey('0')
+ SortKey('started_date')
+ SortKey('finished_date')
+ SortKey('failed_date')
+ SortKey('canceled_date')
+ SortKey('archived_date')
+ SortKey('cancel_policy')
+ SortKey('parent_vacancy')
+ SortKey('lock', path_spec='hash')
+ SortKey('author')
+ SortKey('tenant')
+ SortKey('cert')
)
class CancelPayload(BaseModel):
status: Literal['CANCELED'] = 'CANCELED'
class Course(TypedDict):
id: str
name: str
@router.patch('/<id>', compress=True, tags=['Enrollment'])
def cancel(id: str, payload: CancelPayload):
return {}
class Cancel(BaseModel):
id: UUID4 | str
course: Course
@router.patch(
'/<id>/cancel',
compress=True,
tags=['Enrollment'],
middlewares=[
AuditLogMiddleware('ENROLLMENT_CANCEL', user_collect, ('id', 'course'))
],
)
def cancel(id: str, payload: Cancel):
user: User = router.context['user']
return payload
@router.post('/', compress=True, tags=['Enrollment'])

View File

@@ -22,11 +22,11 @@ LIMIT = 25
@router.get('/', include_in_schema=False)
def settings():
user: User = router.context['user']
acls = user_collect.get_items(
acls = user_collect.query(
KeyPair(user.id, PrefixKey('acls')),
limit=LIMIT,
)
tenants = user_collect.get_items(
tenants = user_collect.query(
KeyPair(user.id, PrefixKey('orgs')),
limit=LIMIT,
)

View File

@@ -21,11 +21,12 @@ from layercake.dynamodb import (
)
from pydantic import UUID4, BaseModel, StringConstraints
import cognito
import elastic
from boto3clients import dynamodb_client
from boto3clients import dynamodb_client, idp_client
from middlewares import AuditLogMiddleware
from models import User
from settings import ELASTIC_CONN, USER_TABLE
from settings import ELASTIC_CONN, USER_POOOL_ID, USER_TABLE
class BadRequestError(MissingError, PowertoolsBadRequestError): ...
@@ -37,12 +38,7 @@ user_collect = DynamoDBCollection(user_layer, exception_cls=BadRequestError)
elastic_client = Elasticsearch(**ELASTIC_CONN)
@router.get(
'/',
compress=True,
tags=['User'],
summary='Get users',
)
@router.get('/', compress=True, tags=['User'], summary='Get users')
def get_users():
event = router.current_event
query = event.get_query_string_value('query', '{}')
@@ -67,29 +63,28 @@ def post_user(payload: User):
return Response(status_code=HTTPStatus.CREATED)
class NewPasswordPayload(BaseModel):
class NewPassword(BaseModel):
cognito_sub: UUID4
new_password: Annotated[str, StringConstraints(min_length=6)]
@router.patch('/<id>', compress=True, tags=['User'])
def patch_reset(id: str, payload: NewPasswordPayload):
def patch_newpassword(id: str, payload: NewPassword):
return Response(status_code=HTTPStatus.OK)
@router.get(
'/<id>',
compress=True,
tags=['User'],
summary='Get user',
)
@router.get('/<id>', compress=True, tags=['User'], summary='Get user')
def get_user(id: str):
return user_collect.get_item(KeyPair(id, '0'))
@router.get('/<id>/idp', compress=True, include_in_schema=False)
def get_idp(id: str):
return []
return cognito.admin_get_user(
sub=id,
user_pool_id=USER_POOOL_ID,
idp_client=idp_client,
)
@router.get(
@@ -99,7 +94,7 @@ def get_idp(id: str):
summary='Get user emails',
)
def get_emails(id: str):
return user_collect.get_items(
return user_collect.query(
KeyPair(id, PrefixKey('emails')),
start_key=router.current_event.get_query_string_value('start_key', None),
)
@@ -112,7 +107,7 @@ def get_emails(id: str):
summary='Get user logs',
)
def get_logs(id: str):
return user_collect.get_items(
return user_collect.query(
# Post-migration: uncomment to enable PartitionKey with a composite key (id with `logs` prefix).
# PartitionKey(ComposeKey(id, prefix='logs')),
PartitionKey(ComposeKey(id, prefix='log', delimiter=':')),
@@ -127,7 +122,7 @@ def get_logs(id: str):
summary='Get user orgs',
)
def get_orgs(id: str):
return user_collect.get_items(
return user_collect.query(
KeyPair(id, PrefixKey('orgs')),
start_key=router.current_event.get_query_string_value('start_key', None),
)

View File

@@ -13,6 +13,8 @@
{"id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "tags#MqiRQWy9cSBEci93aKBvTd"}, "tag": {"S": "Test"}, "create_date": {"S": "2023-09-22T18:31:11.475449-03:00"}}
{"id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "tags#NQ3YmmTesfoSyHCkPKGKEF"}, "tag": {"S": "F\u00e1brica"}, "create_date": {"S": "2023-09-22T18:31:11.475449-03:00"}}
{"id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "tags#bg45io8igarjsA4BzPQyrz"}, "tag": {"S": "Escrit\u00f3rio"}, "create_date": {"S": "2023-09-22T18:31:11.475449-03:00"}}
{"id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "payment_policy"}, "due_days": {"N": "90"}}
{"id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "billing_policy"}, "billing_day": {"N": "2"}, "payment_method": {"S": "PIX"}}
{"sk": {"S": "admin#18606"}, "create_date": {"S": "2023-09-22T18:31:11.475449-03:00"}, "id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "email": {"S": "mateushickmann@petrobras.com.br"}, "name": {"S": "MATEUS LATTIK HICKMANN"}}
{"sk": {"S": "admin#21425"}, "create_date": {"S": "2023-09-22T18:34:06.480230-03:00"}, "id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "email": {"S": "sergio@inbep.com.br"}, "name": {"S": "S\u00e9rgio Siqueira"}}
{"sk": {"S": "admin#2338"}, "create_date": {"S": "2023-09-22T18:28:10.121068-03:00"}, "id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "email": {"S": "empresa@inbep.com.br"}, "name": {"S": "Empresa Teste"}}
@@ -68,3 +70,6 @@
{"id": {"S": "webhook_requests#*#0e7f4d2e62ec525fc94465a6dd7299d2"}, "sk": {"S": "2025-03-03T15:47:42.039256-03:00"}, "request": {"M": {"id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "headers": {"M": {"Accept": {"S": "*/*"}, "Accept-Encoding": {"S": "gzip, deflate"}, "Connection": {"S": "keep-alive"}, "Content-Length": {"S": "108"}, "Content-Type": {"S": "application/json"}, "User-Agent": {"S": "eduseg/python-requests/2.32.3"}}}, "payload": {"M": {"id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "email": {"S": "sergio@somosbeta.com.br"}, "name": {"S": "S\u00e9rgio Rafael de Siqueira"}}}}}, "response": {"M": {"body": {"S": "{\"message\":\"Workflow was started\"}"}, "elapsed_time": {"S": "0.14"}, "headers": {"M": {"alt-svc": {"S": "h3=\":443\"; ma=86400"}, "cf-cache-status": {"S": "DYNAMIC"}, "CF-RAY": {"S": "8fc528b57b62a3f0-GRU"}, "Connection": {"S": "keep-alive"}, "Content-Length": {"S": "34"}, "Content-Type": {"S": "application/json; charset=utf-8"}, "Date": {"S": "Fri, 03 Jan 2025 18:47:44 GMT"}, "etag": {"S": "W/\"22-6OS7cK0FzqnV2NeDHdOSGS1bVUs\""}, "NEL": {"S": "{\"success_fraction\":0,\"report_to\":\"cf-nel\",\"max_age\":604800}"}, "Report-To": {"S": "{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=FMkgKCTOnFvNgtE30yiYnM4XtK8q99O62s7Ep57KDNc3YGiy3W1j%2BzfS0vqJNylSAn5viU3MMGJvTIwPsYQ6jQ298t1p0hYd1KPwURhxchME4hc%2BsO0NNAv%2FFEpXv1ZYWw%3D%3D\"}],\"group\":\"cf-nel\",\"max_age\":604800}"}, "Server": {"S": "cloudflare"}, "server-timing": {"S": "cfL4;desc=\"?proto=TCP&rtt=1016&min_rtt=998&rtt_var=411&sent=4&recv=7&lost=0&retrans=0&sent_bytes=2836&recv_bytes=999&delivery_rate=2522648&cwnd=251&unsent_bytes=0&cid=1b26053952909423&ts=93&x=0\""}, "Strict-Transport-Security": {"S": "max-age=0; includeSubDomains"}, "vary": {"S": "Accept-Encoding"}}}, "status_code": {"S": "200"}}}, "update_date": {"S": "2025-01-03T15:47:44.537597-03:00"}, "url": {"S": "https://n8n.sergio.run/webhook/56bb43b8-533c-4e8b-bdaa-3f7c2b0e548f"}}
{"id": {"S": "webhooks#*"}, "sk": {"S": "1e3759c9c4cfa7aaf86ac281bdb8fd6f"}, "author": {"M": {"id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "name": {"S": "S\u00e9rgio Rafael de Siqueira"}}}, "create_date": {"S": "2025-01-06T15:00:39.363207-03:00"}, "event_type": {"S": "insert"}, "resource": {"S": "users"}, "url": {"S": "https://hook.us2.make.com/hgkc5oj5dbpfld5qvu1xmsu22ond93l9"}}
{"id": {"S": "log:5OxmMjL-ujoR5IMGegQz"},"sk": {"S": "2024-02-26T11:48:17.605911-03:00"},"action": {"S": "OPEN_EMAIL"},"data": {"M": {"ip_address": {"S": "66.249.83.73"},"message_id": {"S": "0103018de5de75cf-670bf01f-7ccf-4ce0-88b4-7c85cd0d06d0-000000"},"recipients": {"L": [{"S": "sergio@somosbeta.com.br"}]},"subject": {"S": "Re: (sem assunto)"},"timestamp": {"S": "2024-02-26T14:48:16.976Z"},"user_agent": {"S": "Mozilla/5.0 (Windows NT 5.1; rv:11.0) Gecko Firefox/11.0 (via ggpht.com GoogleImageProxy)"}}},"ttl": {"N": "1772030897"}}
{"id": {"S": "7zf52CWrTS3csRBFWU5rkq"},"sk": {"S": "0"},"cognito:sub": {"S": "58efed8d-d276-41a8-8502-4ab8b5a6415e"},"cpf": {"S": "04330965275"},"createDate": {"S": "2025-04-08T10:24:46.493980-03:00"},"email": {"S": "barbara.gomes@sinobras.com.br"},"email_verified": {"BOOL": true},"konviva:id": {"N": "199205"},"lastLogin": {"S": "2025-04-10T12:01:48.380215-03:00"},"name": {"S": "Barbara Kamyla Vasconcelos Gomes"},"tenant__org_id": {"SS": ["EkvQwpmmL6vzWtJunM5dCJ"]},"update_date": {"S": "2025-04-10T08:50:22.530758-03:00"}}
{"id": {"S": "7zf52CWrTS3csRBFWU5rkq"},"sk": {"S": "acls#EkvQwpmmL6vzWtJunM5dCJ"},"create_date": {"S": "2025-04-10T09:36:41.133157-03:00"},"roles": {"L": [{"S": "ADMIN"}]}}
{"id": {"S": "7zf52CWrTS3csRBFWU5rkq"},"sk": {"S": "orgs#EkvQwpmmL6vzWtJunM5dCJ"},"cnpj": {"S": "07933914000154"},"create_date": {"S": "2025-04-08T10:24:46.493980-03:00"},"name": {"S": "SIDERURGICA NORTE BRASIL S.A"}}

View File

@@ -32,3 +32,5 @@ match os.getenv('AWS_SAM_LOCAL'), os.getenv('PYTEST_VERSION'):
'cloud_id': ELASTIC_CLOUD_ID,
'basic_auth': ('elastic', ELASTIC_AUTH_PASS),
}
USER_POOOL_ID = 'sa-east-1_s6YmVSfXj'

View File

@@ -23,7 +23,7 @@ Globals:
Architectures:
- x86_64
Layers:
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:35
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:42
Environment:
Variables:
TZ: America/Sao_Paulo
@@ -82,6 +82,15 @@ Resources:
TableName: !Ref CourseTable
- DynamoDBCrudPolicy:
TableName: !Ref OrderTable
- DynamoDBCrudPolicy:
TableName: !Ref EnrollmentTable
- Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- cognito-idp:AdminGetUser
- cognito-idp:AdminSetUserPassword
Resource: !Sub arn:aws:cognito-idp:${AWS::Region}:${AWS::AccountId}:userpool/*
Events:
Preflight:
Type: HttpApi

View File

@@ -59,7 +59,7 @@ def test_post_course(
assert r['statusCode'] == HTTPStatus.CREATED
collect = DynamoDBCollection(dynamodb_persistence_layer)
logs = collect.get_items(
logs = collect.query(
PartitionKey(
ComposeKey('5OxmMjL-ujoR5IMGegQz', prefix='log', delimiter=':'),
)

View File

@@ -8,5 +8,8 @@
{"id": {"S": "log:5OxmMjL-ujoR5IMGegQz"}, "sk": {"S": "2024-02-08T16:42:33.776409-03:00"}, "action": {"S": "OPEN_EMAIL"}}
{"id": {"S": "log:5OxmMjL-ujoR5IMGegQz"}, "sk": {"S": "2019-03-25T00:00:00-03:00"}, "action": {"S": "CLICK_EMAIL"}}
{"id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "0"}, "name": {"S": "EDUSEG"}, "cnpj": {"S": "15608435000190"}, "email": {"S": "org+15608435000190@users.noreply.betaeducacao.com.br"}}
{"id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "payment_policies"}, "due_days": {"N": "90"}}
{"id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "payment_policy"}, "due_days": {"N": "90"}}
{"id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "billing_policy"}, "billing_day": {"N": "1"}, "payment_method": {"S": "PIX"}}
{"id": {"S": "90d7f0d2-d9a4-4467-a31c-f9a7955964cf"}, "sk": {"S": "0"}, "access_period": {"N": "720"}, "create_date": {"S": "2024-12-30T00:00:33.088916-03:00"},"konviva__class_id": {"N": "266"},"name": {"S": "Reciclagem em NR-18 Básico"},"tenant__org_id": {"SS": ["cJtK9SsnJhKPyxESe7g3DG"]}}
{"id": {"S": "43ea4475-c369-4f90-b576-135b7df5106b"}, "sk": {"S": "0"}, "status": {"S": "PENDING"}}
{"id": {"S": "43ea4475-c369-4f90-b576-135b7df5106b"}, "sk": {"S": "cancel_policy"}}

24
http-api/uv.lock generated
View File

@@ -521,7 +521,7 @@ wheels = [
[[package]]
name = "layercake"
version = "0.1.16"
version = "0.2.5"
source = { directory = "../layercake" }
dependencies = [
{ name = "arnparse" },
@@ -538,6 +538,7 @@ dependencies = [
{ name = "pydantic-extra-types" },
{ name = "pytz" },
{ name = "requests" },
{ name = "uuid-utils" },
{ name = "weasyprint" },
]
@@ -557,6 +558,7 @@ requires-dist = [
{ name = "pydantic-extra-types", specifier = ">=2.10.3" },
{ name = "pytz", specifier = ">=2025.1" },
{ name = "requests", specifier = ">=2.32.3" },
{ name = "uuid-utils", specifier = ">=0.10.0" },
{ name = "weasyprint", specifier = ">=65.0" },
]
@@ -938,6 +940,26 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 },
]
[[package]]
name = "uuid-utils"
version = "0.10.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/66/0a/cbdb2eb4845dafeb632d02a18f47b02f87f2ce4f25266f5e3c017976ce89/uuid_utils-0.10.0.tar.gz", hash = "sha256:5db0e1890e8f008657ffe6ded4d9459af724ab114cfe82af1557c87545301539", size = 18828 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/44/54/9d22fa16b19e5d1676eba510f08a9c458d96e2a62ff2c8ebad64251afb18/uuid_utils-0.10.0-cp39-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:8d5a4508feefec62456cd6a41bcdde458d56827d908f226803b886d22a3d5e63", size = 573006 },
{ url = "https://files.pythonhosted.org/packages/08/8e/f895c6e52aa603e521fbc13b8626ba5dd99b6e2f5a55aa96ba5b232f4c53/uuid_utils-0.10.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:dbefc2b9113f9dfe56bdae58301a2b3c53792221410d422826f3d1e3e6555fe7", size = 292543 },
{ url = "https://files.pythonhosted.org/packages/b6/58/cc4834f377a5e97d6e184408ad96d13042308de56643b6e24afe1f6f34df/uuid_utils-0.10.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffc49c33edf87d1ec8112a9b43e4cf55326877716f929c165a2cc307d31c73d5", size = 323340 },
{ url = "https://files.pythonhosted.org/packages/37/e3/6aeddf148f6a7dd7759621b000e8c85382ec83f52ae79b60842d1dc3ab6b/uuid_utils-0.10.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0636b6208f69d5a4e629707ad2a89a04dfa8d1023e1999181f6830646ca048a1", size = 329653 },
{ url = "https://files.pythonhosted.org/packages/0c/00/dd6c2164ace70b7b1671d9129267df331481d7d1e5f9c5e6a564f07953f6/uuid_utils-0.10.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7bc06452856b724df9dedfc161c3582199547da54aeb81915ec2ed54f92d19b0", size = 365471 },
{ url = "https://files.pythonhosted.org/packages/b4/e7/0ab8080fcae5462a7b5e555c1cef3d63457baffb97a59b9bc7b005a3ecb1/uuid_utils-0.10.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:263b2589111c61decdd74a762e8f850c9e4386fb78d2cf7cb4dfc537054cda1b", size = 325844 },
{ url = "https://files.pythonhosted.org/packages/73/39/52d94e9ef75b03f44b39ffc6ac3167e93e74ef4d010a93d25589d9f48540/uuid_utils-0.10.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a558db48b7096de6b4d2d2210d82bba8586a6d55f99106b03bb7d01dc5c5bcd6", size = 344389 },
{ url = "https://files.pythonhosted.org/packages/7c/29/4824566f62666238290d99c62a58e4ab2a8b9cf2eccf94cebd9b3359131e/uuid_utils-0.10.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:807465067f3c892514230326ac71a79b28a8dfe2c88ecd2d5675fc844f3c76b5", size = 510078 },
{ url = "https://files.pythonhosted.org/packages/5e/8f/bbcc7130d652462c685f0d3bd26bb214b754215b476340885a4cb50fb89a/uuid_utils-0.10.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:57423d4a2b9d7b916de6dbd75ba85465a28f9578a89a97f7d3e098d9aa4e5d4a", size = 515937 },
{ url = "https://files.pythonhosted.org/packages/23/f8/34e0c00f5f188604d336713e6a020fcf53b10998e8ab24735a39ab076740/uuid_utils-0.10.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:76d8d660f18ff6b767e319b1b5f927350cd92eafa4831d7ef5b57fdd1d91f974", size = 494111 },
{ url = "https://files.pythonhosted.org/packages/1a/52/b7f0066cc90a7a9c28d54061ed195cd617fde822e5d6ac3ccc88509c3c44/uuid_utils-0.10.0-cp39-abi3-win32.whl", hash = "sha256:6c11a71489338837db0b902b75e1ba7618d5d29f05fde4f68b3f909177dbc226", size = 173520 },
{ url = "https://files.pythonhosted.org/packages/8b/15/f04f58094674d333974243fb45d2c740cf4b79186fb707168e57943c84a3/uuid_utils-0.10.0-cp39-abi3-win_amd64.whl", hash = "sha256:11c55ae64f6c0a7a0c741deae8ca2a4eaa11e9c09dbb7bec2099635696034cf7", size = 182965 },
]
[[package]]
name = "wcwidth"
version = "0.2.13"