add purge to canceled and failed enrollment
This commit is contained in:
@@ -1,8 +1,5 @@
|
|||||||
from typing import Annotated
|
|
||||||
|
|
||||||
from aws_lambda_powertools import Logger
|
from aws_lambda_powertools import Logger
|
||||||
from aws_lambda_powertools.event_handler.api_gateway import Router
|
from aws_lambda_powertools.event_handler.api_gateway import Router
|
||||||
from aws_lambda_powertools.event_handler.openapi.params import Body
|
|
||||||
from layercake.dateutils import now
|
from layercake.dateutils import now
|
||||||
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair
|
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair
|
||||||
|
|
||||||
@@ -17,10 +14,7 @@ dyn = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client)
|
|||||||
|
|
||||||
|
|
||||||
@router.patch('/<enrollment_id>/cancel')
|
@router.patch('/<enrollment_id>/cancel')
|
||||||
def cancel(
|
def cancel(enrollment_id: str):
|
||||||
enrollment_id: str,
|
|
||||||
lock_hash: Annotated[str | None, Body(embed=True)] = None,
|
|
||||||
):
|
|
||||||
now_ = now()
|
now_ = now()
|
||||||
canceled_by: Authenticated = router.context['user']
|
canceled_by: Authenticated = router.context['user']
|
||||||
|
|
||||||
@@ -50,33 +44,7 @@ def cancel(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
transact.delete(
|
transact.delete(
|
||||||
key=KeyPair(
|
key=KeyPair(enrollment_id, 'CANCEL_POLICY'),
|
||||||
pk=enrollment_id,
|
|
||||||
sk='CANCEL_POLICY',
|
|
||||||
),
|
|
||||||
cond_expr='attribute_exists(sk)',
|
cond_expr='attribute_exists(sk)',
|
||||||
exc_cls=CancelPolicyConflictError,
|
exc_cls=CancelPolicyConflictError,
|
||||||
)
|
)
|
||||||
# Remove reminders and policies that no longer apply
|
|
||||||
transact.delete(
|
|
||||||
key=KeyPair(
|
|
||||||
pk=enrollment_id,
|
|
||||||
sk='SCHEDULE#REMINDER_NO_ACCESS_AFTER_3_DAYS',
|
|
||||||
)
|
|
||||||
)
|
|
||||||
transact.delete(
|
|
||||||
key=KeyPair(
|
|
||||||
pk=enrollment_id,
|
|
||||||
sk='SCHEDULE#REMINDER_ACCESS_PERIOD_BEFORE_30_DAYS',
|
|
||||||
)
|
|
||||||
)
|
|
||||||
transact.delete(
|
|
||||||
key=KeyPair(
|
|
||||||
pk=enrollment_id,
|
|
||||||
sk='METADATA#PARENT_SLOT',
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if lock_hash:
|
|
||||||
transact.delete(key=KeyPair(enrollment_id, 'LOCK'))
|
|
||||||
transact.delete(key=KeyPair('LOCK', lock_hash))
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import type { ColumnDef } from '@tanstack/react-table'
|
import type { ColumnDef } from '@tanstack/react-table'
|
||||||
import type { ComponentProps, MouseEvent } from 'react'
|
|
||||||
import { useRequest, useToggle } from 'ahooks'
|
import { useRequest, useToggle } from 'ahooks'
|
||||||
import {
|
import {
|
||||||
CircleXIcon,
|
CircleXIcon,
|
||||||
@@ -9,6 +8,7 @@ import {
|
|||||||
FileBadgeIcon,
|
FileBadgeIcon,
|
||||||
LockOpenIcon
|
LockOpenIcon
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
|
import type { ComponentProps, MouseEvent } from 'react'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -36,8 +36,8 @@ import {
|
|||||||
} from '@repo/ui/components/ui/dropdown-menu'
|
} from '@repo/ui/components/ui/dropdown-menu'
|
||||||
import { Spinner } from '@repo/ui/components/ui/spinner'
|
import { Spinner } from '@repo/ui/components/ui/spinner'
|
||||||
import {
|
import {
|
||||||
type Enrollment,
|
columns as columns_,
|
||||||
columns as columns_
|
type Enrollment
|
||||||
} from '@repo/ui/routes/enrollments/columns'
|
} from '@repo/ui/routes/enrollments/columns'
|
||||||
|
|
||||||
export type { Enrollment }
|
export type { Enrollment }
|
||||||
@@ -123,7 +123,6 @@ function ActionMenu({ row }: { row: any }) {
|
|||||||
<CancelItem
|
<CancelItem
|
||||||
id={row.id}
|
id={row.id}
|
||||||
cancelPolicy={data?.cancel_policy}
|
cancelPolicy={data?.cancel_policy}
|
||||||
lock={data?.lock}
|
|
||||||
onSuccess={onSuccess}
|
onSuccess={onSuccess}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
@@ -256,14 +255,10 @@ const getDaysRemaining = (ttl: number) => {
|
|||||||
function CancelItem({
|
function CancelItem({
|
||||||
id,
|
id,
|
||||||
cancelPolicy,
|
cancelPolicy,
|
||||||
lock,
|
|
||||||
onSuccess,
|
onSuccess,
|
||||||
...props
|
...props
|
||||||
}: ItemProps & {
|
}: ItemProps & {
|
||||||
cancelPolicy?: object
|
cancelPolicy?: object
|
||||||
lock?: {
|
|
||||||
hash: string
|
|
||||||
}
|
|
||||||
}) {
|
}) {
|
||||||
const [loading, { set }] = useToggle(false)
|
const [loading, { set }] = useToggle(false)
|
||||||
const [open, { set: setOpen }] = useToggle(false)
|
const [open, { set: setOpen }] = useToggle(false)
|
||||||
@@ -274,8 +269,7 @@ function CancelItem({
|
|||||||
|
|
||||||
const r = await fetch(`/~/api/enrollments/${id}/cancel`, {
|
const r = await fetch(`/~/api/enrollments/${id}/cancel`, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
headers: new Headers({ 'Content-Type': 'application/json' }),
|
headers: new Headers({ 'Content-Type': 'application/json' })
|
||||||
body: JSON.stringify({ lock_hash: lock?.hash })
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (r.ok) {
|
if (r.ok) {
|
||||||
|
|||||||
@@ -61,6 +61,8 @@ Org = TypedDict('Org', {'org_id': str, 'name': str})
|
|||||||
|
|
||||||
CreatedBy = TypedDict('CreatedBy', {'id': str, 'name': str})
|
CreatedBy = TypedDict('CreatedBy', {'id': str, 'name': str})
|
||||||
|
|
||||||
|
Seat = TypedDict('Seat', {'id': str, 'sk': str})
|
||||||
|
|
||||||
DeduplicationWindow = TypedDict('DeduplicationWindow', {'offset_days': int})
|
DeduplicationWindow = TypedDict('DeduplicationWindow', {'offset_days': int})
|
||||||
|
|
||||||
Subscription = TypedDict(
|
Subscription = TypedDict(
|
||||||
@@ -100,6 +102,11 @@ class SubscriptionFrozenError(Exception):
|
|||||||
super().__init__('Subscription is frozen')
|
super().__init__('Subscription is frozen')
|
||||||
|
|
||||||
|
|
||||||
|
class SeatNotFoundError(Exception):
|
||||||
|
def __init__(self, msg: str | dict):
|
||||||
|
super().__init__('Seat required')
|
||||||
|
|
||||||
|
|
||||||
def enroll(
|
def enroll(
|
||||||
enrollment: Enrollment,
|
enrollment: Enrollment,
|
||||||
*,
|
*,
|
||||||
@@ -108,6 +115,7 @@ def enroll(
|
|||||||
subscription: Subscription | None = None,
|
subscription: Subscription | None = None,
|
||||||
created_by: CreatedBy | None = None,
|
created_by: CreatedBy | None = None,
|
||||||
scheduled_at: datetime | None = None,
|
scheduled_at: datetime | None = None,
|
||||||
|
seat: Seat | None = None,
|
||||||
linked_entities: frozenset[LinkedEntity] = frozenset(),
|
linked_entities: frozenset[LinkedEntity] = frozenset(),
|
||||||
deduplication_window: DeduplicationWindow | None = None,
|
deduplication_window: DeduplicationWindow | None = None,
|
||||||
persistence_layer: DynamoDBPersistenceLayer,
|
persistence_layer: DynamoDBPersistenceLayer,
|
||||||
@@ -138,6 +146,8 @@ def enroll(
|
|||||||
'sk': 'CANCEL_POLICY',
|
'sk': 'CANCEL_POLICY',
|
||||||
'created_at': now_,
|
'created_at': now_,
|
||||||
}
|
}
|
||||||
|
#
|
||||||
|
| ({'seat': seat} if seat else {})
|
||||||
)
|
)
|
||||||
|
|
||||||
# Relationships between this enrollment and its related entities
|
# Relationships between this enrollment and its related entities
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
|||||||
offset_days = old_image.get('dedup_window_offset_days')
|
offset_days = old_image.get('dedup_window_offset_days')
|
||||||
billing_day = old_image.get('subscription_billing_day')
|
billing_day = old_image.get('subscription_billing_day')
|
||||||
created_by = old_image.get('created_by')
|
created_by = old_image.get('created_by')
|
||||||
|
seat = old_image.get('seat')
|
||||||
enrollment = Enrollment(
|
enrollment = Enrollment(
|
||||||
course=old_image['course'],
|
course=old_image['course'],
|
||||||
user=old_image['user'],
|
user=old_image['user'],
|
||||||
@@ -53,6 +54,7 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
|||||||
subscription=subscription,
|
subscription=subscription,
|
||||||
cancel_policy=bool(subscription),
|
cancel_policy=bool(subscription),
|
||||||
created_by=created_by,
|
created_by=created_by,
|
||||||
|
seat=seat,
|
||||||
scheduled_at=datetime.fromisoformat(old_image['scheduled_at']),
|
scheduled_at=datetime.fromisoformat(old_image['scheduled_at']),
|
||||||
# Transfer the deduplication window if it exists
|
# Transfer the deduplication window if it exists
|
||||||
deduplication_window={'offset_days': offset_days} if offset_days else None,
|
deduplication_window={'offset_days': offset_days} if offset_days else None,
|
||||||
|
|||||||
41
enrollments-events/app/events/purge_reminders.py
Normal file
41
enrollments-events/app/events/purge_reminders.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
from aws_lambda_powertools import Logger
|
||||||
|
from aws_lambda_powertools.utilities.data_classes import (
|
||||||
|
EventBridgeEvent,
|
||||||
|
event_source,
|
||||||
|
)
|
||||||
|
from aws_lambda_powertools.utilities.typing import LambdaContext
|
||||||
|
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair
|
||||||
|
from layercake.strutils import md5_hash
|
||||||
|
|
||||||
|
from boto3clients import dynamodb_client
|
||||||
|
from config import ENROLLMENT_TABLE
|
||||||
|
|
||||||
|
logger = Logger(__name__)
|
||||||
|
dyn = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client)
|
||||||
|
|
||||||
|
|
||||||
|
@event_source(data_class=EventBridgeEvent)
|
||||||
|
@logger.inject_lambda_context
|
||||||
|
def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
||||||
|
new_image = event.detail['new_image']
|
||||||
|
enrollment_id = new_image['id']
|
||||||
|
user_id = new_image['user']['id']
|
||||||
|
course_id = new_image['course']['id']
|
||||||
|
lock_hash = md5_hash(f'{user_id}{course_id}')
|
||||||
|
|
||||||
|
with dyn.transact_writer() as transact:
|
||||||
|
transact.delete(
|
||||||
|
key=KeyPair(enrollment_id, 'SCHEDULE#REMINDER_NO_ACCESS_AFTER_3_DAYS')
|
||||||
|
)
|
||||||
|
transact.delete(
|
||||||
|
key=KeyPair(enrollment_id, 'SCHEDULE#REMINDER_NO_ACTIVITY_AFTER_7_DAYS')
|
||||||
|
)
|
||||||
|
transact.delete(
|
||||||
|
key=KeyPair(enrollment_id, 'SCHEDULE#REMINDER_ACCESS_PERIOD_BEFORE_30_DAYS')
|
||||||
|
)
|
||||||
|
transact.delete(key=KeyPair(enrollment_id, 'CANCEL_POLICY'))
|
||||||
|
# Remove locks related to this enrollment
|
||||||
|
transact.delete(key=KeyPair(enrollment_id, 'LOCK'))
|
||||||
|
transact.delete(key=KeyPair('LOCK', lock_hash))
|
||||||
|
|
||||||
|
return True
|
||||||
@@ -326,6 +326,27 @@ Resources:
|
|||||||
sk: ['0']
|
sk: ['0']
|
||||||
status: [PENDING]
|
status: [PENDING]
|
||||||
|
|
||||||
|
EventPurgeRemindersFunction:
|
||||||
|
Type: AWS::Serverless::Function
|
||||||
|
Properties:
|
||||||
|
Handler: events.purge_reminders.lambda_handler
|
||||||
|
LoggingConfig:
|
||||||
|
LogGroup: !Ref EventLog
|
||||||
|
Policies:
|
||||||
|
- DynamoDBCrudPolicy:
|
||||||
|
TableName: !Ref EnrollmentTable
|
||||||
|
Events:
|
||||||
|
DynamoDBEvent:
|
||||||
|
Type: EventBridgeRule
|
||||||
|
Properties:
|
||||||
|
Pattern:
|
||||||
|
resources: [!Ref EnrollmentTable]
|
||||||
|
detail-type: [MODIFY]
|
||||||
|
detail:
|
||||||
|
new_image:
|
||||||
|
sk: ['0']
|
||||||
|
status: [CANCELED, FAILED]
|
||||||
|
|
||||||
# Deprecated
|
# Deprecated
|
||||||
EventSetAccessExpiredFunction:
|
EventSetAccessExpiredFunction:
|
||||||
Type: AWS::Serverless::Function
|
Type: AWS::Serverless::Function
|
||||||
|
|||||||
29
enrollments-events/tests/events/test_purge_reminders.py
Normal file
29
enrollments-events/tests/events/test_purge_reminders.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
from aws_lambda_powertools.utilities.typing import LambdaContext
|
||||||
|
from layercake.dynamodb import DynamoDBPersistenceLayer
|
||||||
|
|
||||||
|
import events.purge_reminders as app
|
||||||
|
|
||||||
|
|
||||||
|
def test_purge_reminders(
|
||||||
|
dynamodb_seeds,
|
||||||
|
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
|
||||||
|
lambda_context: LambdaContext,
|
||||||
|
):
|
||||||
|
event = {
|
||||||
|
'detail': {
|
||||||
|
'new_image': {
|
||||||
|
'id': '14682b79-3df2-4351-9229-8b558af046a0',
|
||||||
|
'sk': '0',
|
||||||
|
'user': {
|
||||||
|
'id': '1234',
|
||||||
|
'name': 'Ozzy Osbourne',
|
||||||
|
'email': 'ozzy@osbourne.com',
|
||||||
|
},
|
||||||
|
'course': {
|
||||||
|
'id': '12334',
|
||||||
|
'name': 'pytest',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert app.lambda_handler(event, lambda_context) # type: ignore
|
||||||
@@ -140,8 +140,6 @@ def set_score(
|
|||||||
id,
|
id,
|
||||||
score,
|
score,
|
||||||
progress=progress,
|
progress=progress,
|
||||||
user_id=user_id,
|
|
||||||
course_id=course_id,
|
|
||||||
dynamodb_persistence_layer=dynamodb_persistence_layer,
|
dynamodb_persistence_layer=dynamodb_persistence_layer,
|
||||||
)
|
)
|
||||||
except EnrollmentConflictError as err:
|
except EnrollmentConflictError as err:
|
||||||
@@ -251,26 +249,6 @@ def _set_status_as_completed(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Remove reminders and policies that no longer apply
|
|
||||||
transact.delete(
|
|
||||||
key=KeyPair(
|
|
||||||
pk=id,
|
|
||||||
sk='CANCEL_POLICY',
|
|
||||||
)
|
|
||||||
)
|
|
||||||
transact.delete(
|
|
||||||
key=KeyPair(
|
|
||||||
pk=id,
|
|
||||||
sk='SCHEDULE#REMINDER_NO_ACTIVITY_AFTER_7_DAYS',
|
|
||||||
)
|
|
||||||
)
|
|
||||||
transact.delete(
|
|
||||||
key=KeyPair(
|
|
||||||
pk=id,
|
|
||||||
sk='SCHEDULE#REMINDER_ACCESS_PERIOD_BEFORE_30_DAYS',
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@@ -279,13 +257,10 @@ def _set_status_as_failed(
|
|||||||
/,
|
/,
|
||||||
score: Decimal,
|
score: Decimal,
|
||||||
progress: Decimal,
|
progress: Decimal,
|
||||||
user_id: str,
|
|
||||||
course_id: str,
|
|
||||||
*,
|
*,
|
||||||
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
|
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
now_ = now()
|
now_ = now()
|
||||||
lock_hash = md5_hash(f'{user_id}{course_id}')
|
|
||||||
|
|
||||||
with dynamodb_persistence_layer.transact_writer() as transact:
|
with dynamodb_persistence_layer.transact_writer() as transact:
|
||||||
transact.update(
|
transact.update(
|
||||||
@@ -308,32 +283,6 @@ def _set_status_as_failed(
|
|||||||
},
|
},
|
||||||
exc_cls=EnrollmentConflictError,
|
exc_cls=EnrollmentConflictError,
|
||||||
)
|
)
|
||||||
# Remove reminders and events that no longer apply
|
|
||||||
transact.delete(
|
|
||||||
key=KeyPair(
|
|
||||||
pk=id,
|
|
||||||
sk='CANCEL_POLICY',
|
|
||||||
)
|
|
||||||
)
|
|
||||||
transact.delete(
|
|
||||||
key=KeyPair(
|
|
||||||
pk=id,
|
|
||||||
sk='SCHEDULE#REMINDER_NO_ACTIVITY_AFTER_7_DAYS',
|
|
||||||
)
|
|
||||||
)
|
|
||||||
transact.delete(
|
|
||||||
key=KeyPair(
|
|
||||||
pk=id,
|
|
||||||
sk='SCHEDULE#REMINDER_ACCESS_PERIOD_BEFORE_30_DAYS',
|
|
||||||
)
|
|
||||||
)
|
|
||||||
# Remove locks related to this enrollment
|
|
||||||
transact.delete(
|
|
||||||
key=KeyPair(pk=id, sk='LOCK'),
|
|
||||||
)
|
|
||||||
transact.delete(
|
|
||||||
key=KeyPair(pk='LOCK', sk=lock_hash),
|
|
||||||
)
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|||||||
@@ -244,6 +244,13 @@ else:
|
|||||||
self.rename_key = rename_key
|
self.rename_key = rename_key
|
||||||
self.remove_prefix = remove_prefix
|
self.remove_prefix = remove_prefix
|
||||||
|
|
||||||
|
if remove_prefix:
|
||||||
|
warnings.warn(
|
||||||
|
'SortKey.remove_prefix() is deprecated and will be removed in the future.',
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Key(ABC, dict):
|
class Key(ABC, dict):
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
@@ -671,9 +678,7 @@ class DynamoDBPersistenceLayer:
|
|||||||
try:
|
try:
|
||||||
r = self.client.query(**attrs)
|
r = self.client.query(**attrs)
|
||||||
except ClientError as err:
|
except ClientError as err:
|
||||||
logger.info(attrs)
|
raise err
|
||||||
logger.exception(err)
|
|
||||||
raise
|
|
||||||
else:
|
else:
|
||||||
return dict(
|
return dict(
|
||||||
items=[deserialize(v) for v in r.get('Items', [])],
|
items=[deserialize(v) for v in r.get('Items', [])],
|
||||||
@@ -696,8 +701,7 @@ class DynamoDBPersistenceLayer:
|
|||||||
r = self.client.get_item(**attrs)
|
r = self.client.get_item(**attrs)
|
||||||
except ClientError as err:
|
except ClientError as err:
|
||||||
logger.info(attrs)
|
logger.info(attrs)
|
||||||
logger.exception(err)
|
raise err
|
||||||
raise
|
|
||||||
else:
|
else:
|
||||||
return deserialize(r.get('Item', {}))
|
return deserialize(r.get('Item', {}))
|
||||||
|
|
||||||
@@ -720,8 +724,7 @@ class DynamoDBPersistenceLayer:
|
|||||||
self.client.put_item(**attrs)
|
self.client.put_item(**attrs)
|
||||||
except ClientError as err:
|
except ClientError as err:
|
||||||
logger.info(attrs)
|
logger.info(attrs)
|
||||||
logger.exception(err)
|
raise err
|
||||||
raise
|
|
||||||
else:
|
else:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -754,8 +757,7 @@ class DynamoDBPersistenceLayer:
|
|||||||
self.client.update_item(**attrs)
|
self.client.update_item(**attrs)
|
||||||
except ClientError as err:
|
except ClientError as err:
|
||||||
logger.info(attrs)
|
logger.info(attrs)
|
||||||
logger.exception(err)
|
raise err
|
||||||
raise
|
|
||||||
else:
|
else:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -790,8 +792,7 @@ class DynamoDBPersistenceLayer:
|
|||||||
self.client.delete_item(**attrs)
|
self.client.delete_item(**attrs)
|
||||||
except ClientError as err:
|
except ClientError as err:
|
||||||
logger.info(attrs)
|
logger.info(attrs)
|
||||||
logger.exception(err)
|
raise err
|
||||||
raise
|
|
||||||
else:
|
else:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -1134,6 +1135,12 @@ class DynamoDBCollection:
|
|||||||
return sk.rename_key
|
return sk.rename_key
|
||||||
|
|
||||||
if isinstance(sk, SortKey):
|
if isinstance(sk, SortKey):
|
||||||
|
warnings.warn(
|
||||||
|
'SortKey.remove_prefix() is deprecated and will be removed '
|
||||||
|
'in the future.',
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
return sk.removeprefix(sk.remove_prefix or '')
|
return sk.removeprefix(sk.remove_prefix or '')
|
||||||
|
|
||||||
return pk
|
return pk
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import re
|
import re
|
||||||
from datetime import date
|
|
||||||
from typing import TYPE_CHECKING, Annotated, Any
|
from typing import TYPE_CHECKING, Annotated, Any
|
||||||
|
|
||||||
import ftfy
|
import ftfy
|
||||||
@@ -74,6 +73,7 @@ class CreditCard(BaseModel):
|
|||||||
>>> cc
|
>>> cc
|
||||||
CreditCard(holder_name='Mike Shinoda', number='4111111111111111', cvv='123', exp_month='01', exp_year='2026')
|
CreditCard(holder_name='Mike Shinoda', number='4111111111111111', cvv='123', exp_month='01', exp_year='2026')
|
||||||
"""
|
"""
|
||||||
|
|
||||||
holder_name: NameStr
|
holder_name: NameStr
|
||||||
number: PaymentCardNumber
|
number: PaymentCardNumber
|
||||||
cvv: str = Field(..., min_length=3)
|
cvv: str = Field(..., min_length=3)
|
||||||
|
|||||||
2
layercake/uv.lock
generated
2
layercake/uv.lock
generated
@@ -836,7 +836,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "layercake"
|
name = "layercake"
|
||||||
version = "0.13.0"
|
version = "0.13.1"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "arnparse" },
|
{ name = "arnparse" },
|
||||||
|
|||||||
Reference in New Issue
Block a user