demo seed change
This commit is contained in:
0
shared/utils/__init__.py
Normal file → Executable file
0
shared/utils/__init__.py
Normal file → Executable file
0
shared/utils/batch_generator.py
Normal file → Executable file
0
shared/utils/batch_generator.py
Normal file → Executable file
0
shared/utils/circuit_breaker.py
Normal file → Executable file
0
shared/utils/circuit_breaker.py
Normal file → Executable file
0
shared/utils/city_normalization.py
Normal file → Executable file
0
shared/utils/city_normalization.py
Normal file → Executable file
465
shared/utils/demo_dates.py
Normal file → Executable file
465
shared/utils/demo_dates.py
Normal file → Executable file
@@ -1,18 +1,37 @@
|
||||
"""
|
||||
Demo Date Offset Utilities
|
||||
Provides functions for adjusting dates during demo session cloning
|
||||
to ensure all temporal data is relative to the demo session creation time
|
||||
Demo Date Utilities for Temporal Determinism
|
||||
Adjusts dates from seed data to be relative to demo session creation time
|
||||
"""
|
||||
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from typing import Optional
|
||||
import pytz
|
||||
|
||||
# Fixed base reference date for all demo data
|
||||
# This is the "day 0" that all seed data is defined relative to
|
||||
BASE_REFERENCE_DATE = datetime(2025, 1, 15, 6, 0, 0, tzinfo=timezone.utc)
|
||||
|
||||
|
||||
# Base reference date for all demo seed data
|
||||
# All seed scripts should use this as the "logical seed date"
|
||||
# IMPORTANT: This should be set to approximately the current date to ensure demo data appears current
|
||||
# Updated to December 1, 2025 to align with current date
|
||||
BASE_REFERENCE_DATE = datetime(2025, 12, 1, 6, 0, 0, tzinfo=timezone.utc)
|
||||
def get_base_reference_date(session_created_at: Optional[datetime] = None) -> datetime:
|
||||
"""
|
||||
Get the base reference date for demo data.
|
||||
|
||||
If session_created_at is provided, calculate relative to it.
|
||||
Otherwise, use current time (for backwards compatibility with seed scripts).
|
||||
|
||||
Returns:
|
||||
Base reference date at 6 AM UTC
|
||||
"""
|
||||
if session_created_at:
|
||||
if session_created_at.tzinfo is None:
|
||||
session_created_at = session_created_at.replace(tzinfo=timezone.utc)
|
||||
# Reference is session creation time at 6 AM that day
|
||||
return session_created_at.replace(
|
||||
hour=6, minute=0, second=0, microsecond=0
|
||||
)
|
||||
# Fallback for seed scripts: use today at 6 AM
|
||||
now = datetime.now(timezone.utc)
|
||||
return now.replace(hour=6, minute=0, second=0, microsecond=0)
|
||||
|
||||
|
||||
def adjust_date_for_demo(
|
||||
@@ -21,31 +40,14 @@ def adjust_date_for_demo(
|
||||
base_reference_date: datetime = BASE_REFERENCE_DATE
|
||||
) -> Optional[datetime]:
|
||||
"""
|
||||
Adjust a date from seed data to be relative to demo session creation time
|
||||
|
||||
This ensures that demo data appears fresh and relevant regardless of when
|
||||
the demo session is created. For example, expiration dates that were "15 days
|
||||
from seed date" will become "15 days from session creation date".
|
||||
|
||||
Args:
|
||||
original_date: The original date from the seed data (or None)
|
||||
session_created_at: When the demo session was created
|
||||
base_reference_date: The logical date when seed data was created (default: 2025-01-15)
|
||||
|
||||
Returns:
|
||||
Adjusted date relative to session creation, or None if original_date was None
|
||||
|
||||
Adjust a date from seed data to be relative to demo session creation time.
|
||||
|
||||
Example:
|
||||
# Seed data created on 2025-01-15
|
||||
# Stock expiration: 2025-01-30 (15 days from seed date)
|
||||
# Demo session created: 2025-10-16
|
||||
# Result: 2025-10-31 (15 days from session date)
|
||||
|
||||
>>> original = datetime(2025, 1, 30, 12, 0, tzinfo=timezone.utc)
|
||||
>>> session = datetime(2025, 10, 16, 10, 0, tzinfo=timezone.utc)
|
||||
>>> adjusted = adjust_date_for_demo(original, session)
|
||||
>>> print(adjusted)
|
||||
2025-10-31 10:00:00+00:00
|
||||
# Seed data created on 2025-12-13 06:00
|
||||
# Stock expiration: 2025-12-28 06:00 (15 days from seed date)
|
||||
# Demo session created: 2025-12-16 10:00
|
||||
# Base reference: 2025-12-16 06:00
|
||||
# Result: 2025-12-31 10:00 (15 days from session date)
|
||||
"""
|
||||
if original_date is None:
|
||||
return None
|
||||
@@ -65,148 +67,281 @@ def adjust_date_for_demo(
|
||||
return session_created_at + offset
|
||||
|
||||
|
||||
def adjust_date_relative_to_now(
|
||||
days_offset: int,
|
||||
hours_offset: int = 0,
|
||||
reference_time: Optional[datetime] = None
|
||||
def calculate_edge_case_times(session_created_at: datetime) -> dict:
|
||||
"""
|
||||
Calculate deterministic edge case times for demo sessions.
|
||||
|
||||
These times are designed to always create specific demo scenarios:
|
||||
- One late delivery (should have arrived hours ago)
|
||||
- One overdue production batch (should have started hours ago)
|
||||
- One in-progress batch (started recently)
|
||||
- One upcoming batch (starts soon)
|
||||
- One arriving-soon delivery (arrives in a few hours)
|
||||
|
||||
Returns:
|
||||
{
|
||||
'late_delivery_expected': session - 4h,
|
||||
'overdue_batch_planned_start': session - 2h,
|
||||
'in_progress_batch_actual_start': session - 1h45m,
|
||||
'upcoming_batch_planned_start': session + 1h30m,
|
||||
'arriving_soon_delivery_expected': session + 2h30m,
|
||||
'evening_batch_planned_start': today 17:00,
|
||||
'tomorrow_morning_planned_start': tomorrow 05:00
|
||||
}
|
||||
"""
|
||||
if session_created_at.tzinfo is None:
|
||||
session_created_at = session_created_at.replace(tzinfo=timezone.utc)
|
||||
|
||||
# Calculate today at 6 AM (base reference)
|
||||
base_reference = get_base_reference_date(session_created_at)
|
||||
|
||||
# Calculate tomorrow at 6 AM
|
||||
tomorrow_base = base_reference + timedelta(days=1)
|
||||
|
||||
return {
|
||||
'late_delivery_expected': session_created_at - timedelta(hours=4),
|
||||
'overdue_batch_planned_start': session_created_at - timedelta(hours=2),
|
||||
'in_progress_batch_actual_start': session_created_at - timedelta(hours=1, minutes=45),
|
||||
'upcoming_batch_planned_start': session_created_at + timedelta(hours=1, minutes=30),
|
||||
'arriving_soon_delivery_expected': session_created_at + timedelta(hours=2, minutes=30),
|
||||
'evening_batch_planned_start': base_reference.replace(hour=17, minute=0, second=0, microsecond=0),
|
||||
'tomorrow_morning_planned_start': tomorrow_base.replace(hour=5, minute=0, second=0, microsecond=0)
|
||||
}
|
||||
|
||||
|
||||
def ensure_future_time(
|
||||
target_time: datetime,
|
||||
reference_time: datetime,
|
||||
min_hours_ahead: float = 1.0
|
||||
) -> datetime:
|
||||
"""
|
||||
Create a date relative to now (or a reference time) with specified offset
|
||||
|
||||
Useful for creating dates during cloning without needing to store seed dates.
|
||||
|
||||
Args:
|
||||
days_offset: Number of days to add (negative for past dates)
|
||||
hours_offset: Number of hours to add (negative for past times)
|
||||
reference_time: Reference datetime (defaults to now)
|
||||
|
||||
Returns:
|
||||
Calculated datetime
|
||||
|
||||
Example:
|
||||
>>> # Create a date 7 days in the future
|
||||
>>> future = adjust_date_relative_to_now(days_offset=7)
|
||||
>>> # Create a date 3 days in the past
|
||||
>>> past = adjust_date_relative_to_now(days_offset=-3)
|
||||
Ensure a target time is in the future relative to reference time.
|
||||
|
||||
If target_time is in the past or too close to reference_time,
|
||||
shift it forward by at least min_hours_ahead.
|
||||
"""
|
||||
if reference_time is None:
|
||||
reference_time = datetime.now(timezone.utc)
|
||||
elif reference_time.tzinfo is None:
|
||||
if target_time.tzinfo is None:
|
||||
target_time = target_time.replace(tzinfo=timezone.utc)
|
||||
if reference_time.tzinfo is None:
|
||||
reference_time = reference_time.replace(tzinfo=timezone.utc)
|
||||
|
||||
return reference_time + timedelta(days=days_offset, hours=hours_offset)
|
||||
|
||||
time_diff = (target_time - reference_time).total_seconds() / 3600
|
||||
|
||||
if time_diff < min_hours_ahead:
|
||||
# Shift forward to ensure minimum hours ahead
|
||||
return reference_time + timedelta(hours=min_hours_ahead)
|
||||
|
||||
return target_time
|
||||
|
||||
|
||||
def calculate_expiration_date(
|
||||
received_date: datetime,
|
||||
shelf_life_days: int
|
||||
) -> datetime:
|
||||
"""
|
||||
Calculate expiration date based on received date and shelf life
|
||||
|
||||
Args:
|
||||
received_date: When the product was received
|
||||
shelf_life_days: Number of days until expiration
|
||||
|
||||
Returns:
|
||||
Calculated expiration datetime
|
||||
"""
|
||||
if received_date.tzinfo is None:
|
||||
received_date = received_date.replace(tzinfo=timezone.utc)
|
||||
|
||||
return received_date + timedelta(days=shelf_life_days)
|
||||
|
||||
|
||||
def get_days_until_expiration(
|
||||
expiration_date: datetime,
|
||||
reference_date: Optional[datetime] = None
|
||||
) -> int:
|
||||
"""
|
||||
Calculate number of days until expiration
|
||||
|
||||
Args:
|
||||
expiration_date: The expiration datetime
|
||||
reference_date: Reference datetime (defaults to now)
|
||||
|
||||
Returns:
|
||||
Number of days until expiration (negative if already expired)
|
||||
"""
|
||||
if reference_date is None:
|
||||
reference_date = datetime.now(timezone.utc)
|
||||
elif reference_date.tzinfo is None:
|
||||
reference_date = reference_date.replace(tzinfo=timezone.utc)
|
||||
|
||||
if expiration_date.tzinfo is None:
|
||||
expiration_date = expiration_date.replace(tzinfo=timezone.utc)
|
||||
|
||||
delta = expiration_date - reference_date
|
||||
return delta.days
|
||||
|
||||
|
||||
def is_expiring_soon(
|
||||
expiration_date: datetime,
|
||||
threshold_days: int = 3,
|
||||
reference_date: Optional[datetime] = None
|
||||
) -> bool:
|
||||
"""
|
||||
Check if a product is expiring soon
|
||||
|
||||
Args:
|
||||
expiration_date: The expiration datetime
|
||||
threshold_days: Number of days to consider as "soon" (default: 3)
|
||||
reference_date: Reference datetime (defaults to now)
|
||||
|
||||
Returns:
|
||||
True if expiring within threshold_days, False otherwise
|
||||
"""
|
||||
days_until = get_days_until_expiration(expiration_date, reference_date)
|
||||
return 0 <= days_until <= threshold_days
|
||||
|
||||
|
||||
def is_expired(
|
||||
expiration_date: datetime,
|
||||
reference_date: Optional[datetime] = None
|
||||
) -> bool:
|
||||
"""
|
||||
Check if a product is expired
|
||||
|
||||
Args:
|
||||
expiration_date: The expiration datetime
|
||||
reference_date: Reference datetime (defaults to now)
|
||||
|
||||
Returns:
|
||||
True if expired, False otherwise
|
||||
"""
|
||||
days_until = get_days_until_expiration(expiration_date, reference_date)
|
||||
return days_until < 0
|
||||
|
||||
|
||||
def adjust_multiple_dates(
|
||||
dates_dict: dict,
|
||||
def resolve_time_marker(
|
||||
time_marker: str,
|
||||
session_created_at: datetime,
|
||||
base_reference_date: datetime = BASE_REFERENCE_DATE
|
||||
) -> dict:
|
||||
) -> datetime:
|
||||
"""
|
||||
Adjust multiple dates in a dictionary
|
||||
|
||||
Resolve time markers like "BASE_TS + 1h30m" to actual datetimes.
|
||||
|
||||
Supports markers in the format:
|
||||
- "BASE_TS + XhYm" (e.g., "BASE_TS + 1h30m")
|
||||
- "BASE_TS - XhYm" (e.g., "BASE_TS - 2h")
|
||||
- "BASE_TS + Xd" (e.g., "BASE_TS + 2d")
|
||||
- "BASE_TS - Xd" (e.g., "BASE_TS - 1d")
|
||||
|
||||
Args:
|
||||
dates_dict: Dictionary with datetime values to adjust
|
||||
session_created_at: When the demo session was created
|
||||
base_reference_date: The logical date when seed data was created
|
||||
|
||||
time_marker: Time marker string to resolve
|
||||
session_created_at: Demo session creation time
|
||||
base_reference_date: Base reference date for calculation
|
||||
|
||||
Returns:
|
||||
Dictionary with adjusted dates (preserves None values)
|
||||
|
||||
Example:
|
||||
>>> dates = {
|
||||
... 'expiration_date': datetime(2025, 1, 30, tzinfo=timezone.utc),
|
||||
... 'received_date': datetime(2025, 1, 15, tzinfo=timezone.utc),
|
||||
... 'optional_date': None
|
||||
... }
|
||||
>>> session = datetime(2025, 10, 16, tzinfo=timezone.utc)
|
||||
>>> adjusted = adjust_multiple_dates(dates, session)
|
||||
Resolved datetime adjusted for demo session
|
||||
|
||||
Raises:
|
||||
ValueError: If time_marker format is invalid
|
||||
|
||||
Examples:
|
||||
>>> resolve_time_marker("BASE_TS + 1h30m", session_time)
|
||||
>>> # Returns session_created_at + 1h30m
|
||||
>>> resolve_time_marker("BASE_TS - 2h", session_time)
|
||||
>>> # Returns session_created_at - 2h
|
||||
"""
|
||||
return {
|
||||
key: adjust_date_for_demo(value, session_created_at, base_reference_date)
|
||||
for key, value in dates_dict.items()
|
||||
}
|
||||
if not time_marker or not time_marker.startswith("BASE_TS"):
|
||||
raise ValueError(f"Invalid time marker format: {time_marker}")
|
||||
|
||||
# Extract the offset part
|
||||
offset_part = time_marker[7:].strip() # Remove "BASE_TS "
|
||||
|
||||
if not offset_part:
|
||||
# Just "BASE_TS" - return session_created_at
|
||||
return session_created_at
|
||||
|
||||
# Parse operator and value
|
||||
operator = offset_part[0]
|
||||
value_part = offset_part[1:].strip()
|
||||
|
||||
if operator not in ['+', '-']:
|
||||
raise ValueError(f"Invalid operator in time marker: {time_marker}")
|
||||
|
||||
# Parse time components
|
||||
days = 0
|
||||
hours = 0
|
||||
minutes = 0
|
||||
|
||||
if 'd' in value_part:
|
||||
# Handle days
|
||||
day_part, rest = value_part.split('d', 1)
|
||||
days = int(day_part)
|
||||
value_part = rest
|
||||
|
||||
if 'h' in value_part:
|
||||
# Handle hours
|
||||
hour_part, rest = value_part.split('h', 1)
|
||||
hours = int(hour_part)
|
||||
value_part = rest
|
||||
|
||||
if 'm' in value_part:
|
||||
# Handle minutes
|
||||
minute_part = value_part.split('m', 1)[0]
|
||||
minutes = int(minute_part)
|
||||
|
||||
# Calculate offset
|
||||
offset = timedelta(days=days, hours=hours, minutes=minutes)
|
||||
|
||||
if operator == '+':
|
||||
return session_created_at + offset
|
||||
else:
|
||||
return session_created_at - offset
|
||||
|
||||
|
||||
def shift_to_session_time(
|
||||
original_offset_days: int,
|
||||
original_hour: int,
|
||||
original_minute: int,
|
||||
session_created_at: datetime,
|
||||
base_reference: Optional[datetime] = None
|
||||
) -> datetime:
|
||||
"""
|
||||
Shift a time from seed data to demo session time with same-day preservation.
|
||||
|
||||
Ensures that:
|
||||
1. Items scheduled for "today" (offset_days=0) remain on the same day as session creation
|
||||
2. Future items stay in the future, past items stay in the past
|
||||
3. Times don't shift to invalid moments (e.g., past times for pending items)
|
||||
|
||||
Examples:
|
||||
# Session created at noon, item originally scheduled for morning
|
||||
>>> session = datetime(2025, 12, 12, 12, 0, tzinfo=timezone.utc)
|
||||
>>> result = shift_to_session_time(0, 6, 0, session) # Today at 06:00
|
||||
>>> # Returns today at 13:00 (shifted forward to stay in future)
|
||||
|
||||
# Session created at noon, item originally scheduled for evening
|
||||
>>> result = shift_to_session_time(0, 18, 0, session) # Today at 18:00
|
||||
>>> # Returns today at 18:00 (already in future)
|
||||
"""
|
||||
if session_created_at.tzinfo is None:
|
||||
session_created_at = session_created_at.replace(tzinfo=timezone.utc)
|
||||
|
||||
if base_reference is None:
|
||||
base_reference = get_base_reference_date(session_created_at)
|
||||
|
||||
# Calculate original time
|
||||
original_time = base_reference.replace(
|
||||
hour=original_hour,
|
||||
minute=original_minute,
|
||||
second=0,
|
||||
microsecond=0
|
||||
) + timedelta(days=original_offset_days)
|
||||
|
||||
# Calculate offset from base reference
|
||||
offset = original_time - base_reference
|
||||
|
||||
# Apply offset to session creation date
|
||||
new_time = session_created_at + offset
|
||||
|
||||
# Ensure the time is in the future for pending items
|
||||
if original_offset_days >= 0: # Future or today
|
||||
new_time = ensure_future_time(new_time, session_created_at, min_hours_ahead=0.5)
|
||||
|
||||
return new_time
|
||||
|
||||
|
||||
def get_working_hours_time(
|
||||
target_date: datetime,
|
||||
hours_from_start: float = 2.0
|
||||
) -> datetime:
|
||||
"""
|
||||
Get a time within working hours (8 AM - 6 PM) for a given date.
|
||||
|
||||
Args:
|
||||
target_date: The date to calculate time for
|
||||
hours_from_start: Hours from working day start (8 AM)
|
||||
|
||||
Returns:
|
||||
Datetime within working hours
|
||||
"""
|
||||
if target_date.tzinfo is None:
|
||||
target_date = target_date.replace(tzinfo=timezone.utc)
|
||||
|
||||
# Working hours: 8 AM - 6 PM (10 hours)
|
||||
working_start = target_date.replace(hour=8, minute=0, second=0, microsecond=0)
|
||||
working_end = target_date.replace(hour=18, minute=0, second=0, microsecond=0)
|
||||
|
||||
# Calculate time within working hours
|
||||
result_time = working_start + timedelta(hours=hours_from_start)
|
||||
|
||||
# Ensure it's within working hours
|
||||
if result_time > working_end:
|
||||
result_time = working_end
|
||||
|
||||
return result_time
|
||||
|
||||
|
||||
def get_next_workday(date: datetime) -> datetime:
|
||||
"""
|
||||
Get the next workday (Monday-Friday), skipping weekends.
|
||||
|
||||
If date is Friday, returns Monday.
|
||||
If date is Saturday, returns Monday.
|
||||
Otherwise returns next day.
|
||||
"""
|
||||
if date.tzinfo is None:
|
||||
date = date.replace(tzinfo=timezone.utc)
|
||||
|
||||
next_day = date + timedelta(days=1)
|
||||
|
||||
# Skip weekends
|
||||
while next_day.weekday() >= 5: # 5=Saturday, 6=Sunday
|
||||
next_day += timedelta(days=1)
|
||||
|
||||
return next_day
|
||||
|
||||
|
||||
def get_previous_workday(date: datetime) -> datetime:
|
||||
"""
|
||||
Get the previous workday (Monday-Friday), skipping weekends.
|
||||
|
||||
If date is Monday, returns Friday.
|
||||
If date is Sunday, returns Friday.
|
||||
Otherwise returns previous day.
|
||||
"""
|
||||
if date.tzinfo is None:
|
||||
date = date.replace(tzinfo=timezone.utc)
|
||||
|
||||
prev_day = date - timedelta(days=1)
|
||||
|
||||
# Skip weekends
|
||||
while prev_day.weekday() >= 5: # 5=Saturday, 6=Sunday
|
||||
prev_day -= timedelta(days=1)
|
||||
|
||||
return prev_day
|
||||
|
||||
|
||||
def format_iso_with_timezone(dt: datetime) -> str:
|
||||
"""
|
||||
Format datetime as ISO 8601 with timezone, replacing Z with +00:00 for compatibility.
|
||||
"""
|
||||
if dt.tzinfo is None:
|
||||
dt = dt.replace(tzinfo=timezone.utc)
|
||||
|
||||
iso_string = dt.isoformat()
|
||||
return iso_string.replace('+00:00', 'Z') if iso_string.endswith('+00:00') else iso_string
|
||||
113
shared/utils/demo_id_transformer.py
Normal file
113
shared/utils/demo_id_transformer.py
Normal file
@@ -0,0 +1,113 @@
|
||||
"""
|
||||
Demo ID Transformer Utility
|
||||
|
||||
Provides XOR-based ID transformation for creating unique but deterministic
|
||||
IDs across different demo tenants while maintaining cross-service consistency.
|
||||
|
||||
This ensures that:
|
||||
1. Same base ID + same tenant ID = same transformed ID (deterministic)
|
||||
2. Different tenant IDs = different transformed IDs (isolation)
|
||||
3. Cross-service relationships are preserved (consistency)
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from typing import Union
|
||||
|
||||
|
||||
def transform_id(base_id: Union[str, uuid.UUID], tenant_id: Union[str, uuid.UUID]) -> uuid.UUID:
|
||||
"""
|
||||
Transform a base ID using XOR with tenant ID to create unique but deterministic IDs.
|
||||
|
||||
Args:
|
||||
base_id: Original UUID (string or UUID object)
|
||||
tenant_id: Tenant UUID (string or UUID object)
|
||||
|
||||
Returns:
|
||||
Transformed UUID that is unique to this tenant but deterministic
|
||||
|
||||
Example:
|
||||
>>> base_uuid = UUID('10000000-0000-0000-0000-000000000001')
|
||||
>>> tenant_uuid = UUID('a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6')
|
||||
>>> transform_id(base_uuid, tenant_uuid)
|
||||
# Returns deterministic UUID based on XOR of the two
|
||||
"""
|
||||
# Convert inputs to UUID objects if they aren't already
|
||||
if isinstance(base_id, str):
|
||||
base_uuid = uuid.UUID(base_id)
|
||||
else:
|
||||
base_uuid = base_id
|
||||
|
||||
if isinstance(tenant_id, str):
|
||||
tenant_uuid = uuid.UUID(tenant_id)
|
||||
else:
|
||||
tenant_uuid = tenant_id
|
||||
|
||||
# Convert UUIDs to 16-byte arrays
|
||||
base_bytes = base_uuid.bytes
|
||||
tenant_bytes = tenant_uuid.bytes
|
||||
|
||||
# Apply XOR transformation
|
||||
transformed_bytes = bytes(b1 ^ b2 for b1, b2 in zip(base_bytes, tenant_bytes))
|
||||
|
||||
# Create new UUID from transformed bytes
|
||||
transformed_uuid = uuid.UUID(bytes=transformed_bytes)
|
||||
|
||||
return transformed_uuid
|
||||
|
||||
|
||||
def generate_deterministic_uuid_from_string(input_string: str, tenant_id: Union[str, uuid.UUID]) -> uuid.UUID:
|
||||
"""
|
||||
Generate a deterministic UUID from a string input and tenant ID.
|
||||
|
||||
Useful for transforming non-UUID identifiers (like SKUs) into UUIDs
|
||||
while maintaining determinism across services.
|
||||
|
||||
Args:
|
||||
input_string: String identifier (e.g., SKU, product code)
|
||||
tenant_id: Tenant UUID for isolation
|
||||
|
||||
Returns:
|
||||
Deterministic UUID based on the input string and tenant
|
||||
"""
|
||||
if isinstance(tenant_id, str):
|
||||
tenant_uuid = uuid.UUID(tenant_id)
|
||||
else:
|
||||
tenant_uuid = tenant_id
|
||||
|
||||
# Create a combined string for hashing
|
||||
combined = f"{input_string}-{tenant_uuid}"
|
||||
|
||||
# Use SHA-256 hash to create deterministic UUID
|
||||
import hashlib
|
||||
hash_obj = hashlib.sha256(combined.encode('utf-8'))
|
||||
|
||||
# Use first 16 bytes for UUID v5 namespace
|
||||
hash_bytes = hash_obj.digest()[:16]
|
||||
|
||||
# Create UUID v5 using a standard namespace
|
||||
namespace_uuid = uuid.NAMESPACE_DNS # Using DNS namespace as base
|
||||
deterministic_uuid = uuid.uuid5(namespace_uuid, combined)
|
||||
|
||||
return deterministic_uuid
|
||||
|
||||
|
||||
# Utility functions for common transformations
|
||||
|
||||
def transform_ingredient_id(base_ingredient_id: Union[str, uuid.UUID], tenant_id: Union[str, uuid.UUID]) -> uuid.UUID:
|
||||
"""Transform an ingredient ID for a specific tenant"""
|
||||
return transform_id(base_ingredient_id, tenant_id)
|
||||
|
||||
|
||||
def transform_recipe_id(base_recipe_id: Union[str, uuid.UUID], tenant_id: Union[str, uuid.UUID]) -> uuid.UUID:
|
||||
"""Transform a recipe ID for a specific tenant"""
|
||||
return transform_id(base_recipe_id, tenant_id)
|
||||
|
||||
|
||||
def transform_supplier_id(base_supplier_id: Union[str, uuid.UUID], tenant_id: Union[str, uuid.UUID]) -> uuid.UUID:
|
||||
"""Transform a supplier ID for a specific tenant"""
|
||||
return transform_id(base_supplier_id, tenant_id)
|
||||
|
||||
|
||||
def transform_production_batch_id(base_batch_id: Union[str, uuid.UUID], tenant_id: Union[str, uuid.UUID]) -> uuid.UUID:
|
||||
"""Transform a production batch ID for a specific tenant"""
|
||||
return transform_id(base_batch_id, tenant_id)
|
||||
0
shared/utils/optimization.py
Normal file → Executable file
0
shared/utils/optimization.py
Normal file → Executable file
0
shared/utils/saga_pattern.py
Normal file → Executable file
0
shared/utils/saga_pattern.py
Normal file → Executable file
79
shared/utils/seed_data_paths.py
Normal file
79
shared/utils/seed_data_paths.py
Normal file
@@ -0,0 +1,79 @@
|
||||
"""
|
||||
Seed Data Path Utilities
|
||||
Provides functions to locate seed data files for demo data creation
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
import os
|
||||
|
||||
|
||||
def get_seed_data_path(profile: str, filename: str, child_profile: str = None) -> Path:
|
||||
"""
|
||||
Get the path to a seed data file, searching in multiple locations.
|
||||
|
||||
Args:
|
||||
profile: Demo profile (professional/enterprise)
|
||||
filename: Seed data filename
|
||||
child_profile: Optional child profile for enterprise demos
|
||||
|
||||
Returns:
|
||||
Path to the seed data file
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If seed data file cannot be found in any location
|
||||
"""
|
||||
# Search locations in order of priority
|
||||
search_locations = []
|
||||
|
||||
# 1. First check in shared/demo/fixtures (new location)
|
||||
if child_profile:
|
||||
# Enterprise child profile
|
||||
search_locations.append(
|
||||
Path(__file__).parent.parent / "demo" / "fixtures" / profile / child_profile / filename
|
||||
)
|
||||
else:
|
||||
# Regular profile
|
||||
search_locations.append(
|
||||
Path(__file__).parent.parent / "demo" / "fixtures" / profile / filename
|
||||
)
|
||||
|
||||
# 2. Check in infrastructure/seed-data (old location)
|
||||
if child_profile:
|
||||
search_locations.append(
|
||||
Path(__file__).parent.parent.parent / "infrastructure" / "seed-data" / profile / "children" / f"{child_profile}.json"
|
||||
)
|
||||
else:
|
||||
search_locations.append(
|
||||
Path(__file__).parent.parent.parent / "infrastructure" / "seed-data" / profile / filename
|
||||
)
|
||||
|
||||
# 3. Check in infrastructure/seed-data with alternative paths
|
||||
if profile == "enterprise" and not child_profile:
|
||||
search_locations.append(
|
||||
Path(__file__).parent.parent.parent / "infrastructure" / "seed-data" / profile / "parent" / filename
|
||||
)
|
||||
# Also check the shared/demo/fixtures/enterprise/parent directory
|
||||
search_locations.append(
|
||||
Path(__file__).parent.parent / "demo" / "fixtures" / profile / "parent" / filename
|
||||
)
|
||||
|
||||
# Find the first existing file
|
||||
for file_path in search_locations:
|
||||
if file_path.exists():
|
||||
return file_path
|
||||
|
||||
# If no file found, raise an error with all searched locations
|
||||
searched_paths = "\n".join([str(p) for p in search_locations])
|
||||
raise FileNotFoundError(
|
||||
f"Seed data file not found: {filename}\n"
|
||||
f"Profile: {profile}\n"
|
||||
f"Child profile: {child_profile}\n"
|
||||
f"Searched locations:\n{searched_paths}"
|
||||
)
|
||||
|
||||
|
||||
def get_demo_fixture_path(profile: str, filename: str, child_profile: str = None) -> Path:
|
||||
"""
|
||||
Alternative function name for backward compatibility
|
||||
"""
|
||||
return get_seed_data_path(profile, filename, child_profile)
|
||||
0
shared/utils/tenant_settings_client.py
Normal file → Executable file
0
shared/utils/tenant_settings_client.py
Normal file → Executable file
0
shared/utils/time_series_utils.py
Normal file → Executable file
0
shared/utils/time_series_utils.py
Normal file → Executable file
0
shared/utils/validation.py
Normal file → Executable file
0
shared/utils/validation.py
Normal file → Executable file
Reference in New Issue
Block a user