Initial commit - production deployment
This commit is contained in:
310
shared/routing/route_builder.py
Executable file
310
shared/routing/route_builder.py
Executable file
@@ -0,0 +1,310 @@
|
||||
"""
|
||||
Route Builder for standardized URL structure
|
||||
Ensures consistent API patterns across all microservices
|
||||
"""
|
||||
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class RouteCategory(Enum):
|
||||
"""Categories of API routes with different access patterns"""
|
||||
BASE = "base" # Atomic CRUD operations on resources
|
||||
DASHBOARD = "dashboard" # Dashboard data and summaries
|
||||
ANALYTICS = "analytics" # Analytics endpoints (tier-gated)
|
||||
OPERATIONS = "operations" # Service-specific operations
|
||||
|
||||
|
||||
class RouteBuilder:
|
||||
"""
|
||||
Builder for creating standardized API routes
|
||||
|
||||
URL Structure:
|
||||
- Base: /api/v1/tenants/{tenant_id}/{service}/{resource}
|
||||
- Dashboard: /api/v1/tenants/{tenant_id}/{service}/dashboard/{operation}
|
||||
- Analytics: /api/v1/tenants/{tenant_id}/{service}/analytics/{operation}
|
||||
- Operations: /api/v1/tenants/{tenant_id}/{service}/operations/{operation}
|
||||
"""
|
||||
|
||||
API_VERSION = "v1"
|
||||
BASE_PATH = f"/api/{API_VERSION}"
|
||||
|
||||
def __init__(self, service_name: str):
|
||||
"""
|
||||
Initialize route builder for a specific service
|
||||
|
||||
Args:
|
||||
service_name: Name of the service (e.g., 'inventory', 'production')
|
||||
"""
|
||||
self.service_name = service_name
|
||||
|
||||
def build_base_route(self, resource: str, include_tenant_prefix: bool = True) -> str:
|
||||
"""
|
||||
Build base CRUD route for a resource
|
||||
|
||||
Args:
|
||||
resource: Resource name (e.g., 'ingredients', 'batches')
|
||||
Empty string returns just the service path
|
||||
include_tenant_prefix: Whether to include /tenants/{tenant_id} prefix
|
||||
|
||||
Returns:
|
||||
Route path
|
||||
|
||||
Example:
|
||||
builder = RouteBuilder('inventory')
|
||||
builder.build_base_route('ingredients')
|
||||
# Returns: '/api/v1/tenants/{tenant_id}/inventory/ingredients'
|
||||
|
||||
builder.build_base_route('')
|
||||
# Returns: '/api/v1/tenants/{tenant_id}/inventory'
|
||||
"""
|
||||
if resource:
|
||||
# Resource provided - add it to the path
|
||||
if include_tenant_prefix:
|
||||
return f"{self.BASE_PATH}/tenants/{{tenant_id}}/{self.service_name}/{resource}"
|
||||
return f"{self.BASE_PATH}/{self.service_name}/{resource}"
|
||||
else:
|
||||
# Empty resource - return just the service path
|
||||
if include_tenant_prefix:
|
||||
return f"{self.BASE_PATH}/tenants/{{tenant_id}}/{self.service_name}"
|
||||
return f"{self.BASE_PATH}/{self.service_name}"
|
||||
|
||||
def build_dashboard_route(self, operation: str, include_tenant_prefix: bool = True) -> str:
|
||||
"""
|
||||
Build dashboard route
|
||||
|
||||
Args:
|
||||
operation: Dashboard operation (e.g., 'summary', 'capacity-status')
|
||||
include_tenant_prefix: Whether to include /tenants/{tenant_id} prefix
|
||||
|
||||
Returns:
|
||||
Route path
|
||||
|
||||
Example:
|
||||
builder = RouteBuilder('production')
|
||||
builder.build_dashboard_route('summary')
|
||||
# Returns: '/api/v1/tenants/{tenant_id}/production/dashboard/summary'
|
||||
"""
|
||||
if include_tenant_prefix:
|
||||
return f"{self.BASE_PATH}/tenants/{{tenant_id}}/{self.service_name}/dashboard/{operation}"
|
||||
return f"{self.BASE_PATH}/{self.service_name}/dashboard/{operation}"
|
||||
|
||||
def build_analytics_route(self, operation: str, include_tenant_prefix: bool = True) -> str:
|
||||
"""
|
||||
Build analytics route (tier-gated: professional/enterprise)
|
||||
|
||||
Args:
|
||||
operation: Analytics operation (e.g., 'equipment-efficiency', 'trends')
|
||||
include_tenant_prefix: Whether to include /tenants/{tenant_id} prefix
|
||||
|
||||
Returns:
|
||||
Route path
|
||||
|
||||
Example:
|
||||
builder = RouteBuilder('production')
|
||||
builder.build_analytics_route('equipment-efficiency')
|
||||
# Returns: '/api/v1/tenants/{tenant_id}/production/analytics/equipment-efficiency'
|
||||
"""
|
||||
if include_tenant_prefix:
|
||||
return f"{self.BASE_PATH}/tenants/{{tenant_id}}/{self.service_name}/analytics/{operation}"
|
||||
return f"{self.BASE_PATH}/{self.service_name}/analytics/{operation}"
|
||||
|
||||
def build_operations_route(self, operation: str, include_tenant_prefix: bool = True) -> str:
|
||||
"""
|
||||
Build service operations route
|
||||
|
||||
Args:
|
||||
operation: Operation name (e.g., 'schedule-batch', 'stock-adjustment')
|
||||
include_tenant_prefix: Whether to include /tenants/{tenant_id} prefix
|
||||
|
||||
Returns:
|
||||
Route path
|
||||
|
||||
Example:
|
||||
builder = RouteBuilder('production')
|
||||
builder.build_operations_route('schedule-batch')
|
||||
# Returns: '/api/v1/tenants/{tenant_id}/production/operations/schedule-batch'
|
||||
"""
|
||||
if include_tenant_prefix:
|
||||
return f"{self.BASE_PATH}/tenants/{{tenant_id}}/{self.service_name}/operations/{operation}"
|
||||
return f"{self.BASE_PATH}/{self.service_name}/operations/{operation}"
|
||||
|
||||
def build_resource_detail_route(self, resource: str, id_param: str = "id", include_tenant_prefix: bool = True) -> str:
|
||||
"""
|
||||
Build route for individual resource details
|
||||
|
||||
Args:
|
||||
resource: Resource name
|
||||
id_param: Name of the ID parameter
|
||||
include_tenant_prefix: Whether to include /tenants/{tenant_id} prefix
|
||||
|
||||
Returns:
|
||||
Route path
|
||||
|
||||
Example:
|
||||
builder = RouteBuilder('inventory')
|
||||
builder.build_resource_detail_route('ingredients', 'ingredient_id')
|
||||
# Returns: '/api/v1/tenants/{tenant_id}/inventory/ingredients/{ingredient_id}'
|
||||
"""
|
||||
base = self.build_base_route(resource, include_tenant_prefix)
|
||||
return f"{base}/{{{id_param}}}"
|
||||
|
||||
def build_nested_resource_route(
|
||||
self,
|
||||
parent_resource: str,
|
||||
parent_id_param: str,
|
||||
child_resource: str,
|
||||
include_tenant_prefix: bool = True
|
||||
) -> str:
|
||||
"""
|
||||
Build route for nested resources
|
||||
|
||||
Args:
|
||||
parent_resource: Parent resource name
|
||||
parent_id_param: Parent ID parameter name
|
||||
child_resource: Child resource name
|
||||
include_tenant_prefix: Whether to include /tenants/{tenant_id} prefix
|
||||
|
||||
Returns:
|
||||
Route path
|
||||
|
||||
Example:
|
||||
builder = RouteBuilder('inventory')
|
||||
builder.build_nested_resource_route('ingredients', 'ingredient_id', 'stock')
|
||||
# Returns: '/api/v1/tenants/{tenant_id}/inventory/ingredients/{ingredient_id}/stock'
|
||||
"""
|
||||
base = self.build_resource_detail_route(parent_resource, parent_id_param, include_tenant_prefix)
|
||||
return f"{base}/{child_resource}"
|
||||
|
||||
def build_resource_action_route(
|
||||
self,
|
||||
resource: str,
|
||||
id_param: str,
|
||||
action: str,
|
||||
include_tenant_prefix: bool = True
|
||||
) -> str:
|
||||
"""
|
||||
Build route for resource-specific actions
|
||||
|
||||
Args:
|
||||
resource: Resource name
|
||||
id_param: ID parameter name
|
||||
action: Action name
|
||||
include_tenant_prefix: Whether to include /tenants/{tenant_id} prefix
|
||||
|
||||
Returns:
|
||||
Route path
|
||||
|
||||
Example:
|
||||
builder = RouteBuilder('pos')
|
||||
builder.build_resource_action_route('configurations', 'config_id', 'test-connection')
|
||||
# Returns: '/api/v1/tenants/{tenant_id}/pos/configurations/{config_id}/test-connection'
|
||||
"""
|
||||
base = self.build_resource_detail_route(resource, id_param, include_tenant_prefix)
|
||||
return f"{base}/{action}"
|
||||
|
||||
def build_global_route(self, path: str) -> str:
|
||||
"""
|
||||
Build global route without tenant context
|
||||
|
||||
Args:
|
||||
path: Path after service name
|
||||
|
||||
Returns:
|
||||
Route path
|
||||
|
||||
Example:
|
||||
builder = RouteBuilder('pos')
|
||||
builder.build_global_route('supported-systems')
|
||||
# Returns: '/api/v1/pos/supported-systems'
|
||||
"""
|
||||
return f"{self.BASE_PATH}/{self.service_name}/{path}"
|
||||
|
||||
def build_webhook_route(self, path: str) -> str:
|
||||
"""
|
||||
Build webhook route (no tenant context)
|
||||
|
||||
Args:
|
||||
path: Webhook path
|
||||
|
||||
Returns:
|
||||
Route path
|
||||
|
||||
Example:
|
||||
builder = RouteBuilder('pos')
|
||||
builder.build_webhook_route('{pos_system}')
|
||||
# Returns: '/api/v1/webhooks/{pos_system}'
|
||||
"""
|
||||
return f"{self.BASE_PATH}/webhooks/{path}"
|
||||
|
||||
def build_custom_route(
|
||||
self,
|
||||
category: RouteCategory,
|
||||
path_segments: list,
|
||||
include_tenant_prefix: bool = True
|
||||
) -> str:
|
||||
"""
|
||||
Build custom route with specified category and path segments
|
||||
|
||||
Args:
|
||||
category: Route category
|
||||
path_segments: List of path segments after category
|
||||
include_tenant_prefix: Whether to include /tenants/{tenant_id} prefix
|
||||
|
||||
Returns:
|
||||
Route path
|
||||
|
||||
Example:
|
||||
builder = RouteBuilder('inventory')
|
||||
builder.build_custom_route(RouteCategory.DASHBOARD, ['food-safety', 'compliance'])
|
||||
# Returns: '/api/v1/tenants/{tenant_id}/inventory/dashboard/food-safety/compliance'
|
||||
"""
|
||||
base_prefix = f"{self.BASE_PATH}/tenants/{{tenant_id}}" if include_tenant_prefix else self.BASE_PATH
|
||||
|
||||
# Build path segments string, avoiding trailing slash when empty
|
||||
segments_str = '/'.join(path_segments) if path_segments else ''
|
||||
separator = '/' if segments_str else ''
|
||||
|
||||
if category == RouteCategory.BASE:
|
||||
return f"{base_prefix}/{self.service_name}{separator}{segments_str}"
|
||||
elif category == RouteCategory.DASHBOARD:
|
||||
return f"{base_prefix}/{self.service_name}/dashboard{separator}{segments_str}"
|
||||
elif category == RouteCategory.ANALYTICS:
|
||||
return f"{base_prefix}/{self.service_name}/analytics{separator}{segments_str}"
|
||||
elif category == RouteCategory.OPERATIONS:
|
||||
return f"{base_prefix}/{self.service_name}/operations{separator}{segments_str}"
|
||||
|
||||
# Fallback to base
|
||||
return f"{base_prefix}/{self.service_name}{separator}{segments_str}"
|
||||
|
||||
@staticmethod
|
||||
def get_route_pattern(category: RouteCategory, service_name: Optional[str] = None) -> str:
|
||||
"""
|
||||
Get regex pattern for matching routes of a specific category
|
||||
|
||||
Args:
|
||||
category: Route category
|
||||
service_name: Optional service name to filter
|
||||
|
||||
Returns:
|
||||
Regex pattern for route matching
|
||||
|
||||
Example:
|
||||
RouteBuilder.get_route_pattern(RouteCategory.ANALYTICS)
|
||||
# Returns: r'^/api/v1/tenants/[^/]+/[^/]+/analytics/.*'
|
||||
|
||||
RouteBuilder.get_route_pattern(RouteCategory.ANALYTICS, 'production')
|
||||
# Returns: r'^/api/v1/tenants/[^/]+/production/analytics/.*'
|
||||
"""
|
||||
service_pattern = service_name if service_name else "[^/]+"
|
||||
|
||||
if category == RouteCategory.BASE:
|
||||
return rf"^/api/v1/tenants/[^/]+/{service_pattern}/(?!dashboard|analytics|operations)[^/]+.*"
|
||||
elif category == RouteCategory.DASHBOARD:
|
||||
return rf"^/api/v1/tenants/[^/]+/{service_pattern}/dashboard/.*"
|
||||
elif category == RouteCategory.ANALYTICS:
|
||||
return rf"^/api/v1/tenants/[^/]+/{service_pattern}/analytics/.*"
|
||||
elif category == RouteCategory.OPERATIONS:
|
||||
return rf"^/api/v1/tenants/[^/]+/{service_pattern}/operations/.*"
|
||||
|
||||
return rf"^/api/v1/tenants/[^/]+/{service_pattern}/.*"
|
||||
Reference in New Issue
Block a user