REFACTOR ALL APIs
This commit is contained in:
15
shared/routing/__init__.py
Normal file
15
shared/routing/__init__.py
Normal file
@@ -0,0 +1,15 @@
|
||||
"""
|
||||
Shared routing utilities for consistent URL structure across services
|
||||
"""
|
||||
|
||||
from shared.routing.route_builder import RouteBuilder, RouteCategory
|
||||
from shared.routing.route_helpers import build_base_route, build_dashboard_route, build_analytics_route, build_operations_route
|
||||
|
||||
__all__ = [
|
||||
'RouteBuilder',
|
||||
'RouteCategory',
|
||||
'build_base_route',
|
||||
'build_dashboard_route',
|
||||
'build_analytics_route',
|
||||
'build_operations_route',
|
||||
]
|
||||
295
shared/routing/route_builder.py
Normal file
295
shared/routing/route_builder.py
Normal file
@@ -0,0 +1,295 @@
|
||||
"""
|
||||
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')
|
||||
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'
|
||||
"""
|
||||
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}"
|
||||
|
||||
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
|
||||
|
||||
if category == RouteCategory.BASE:
|
||||
return f"{base_prefix}/{self.service_name}/{'/'.join(path_segments)}"
|
||||
elif category == RouteCategory.DASHBOARD:
|
||||
return f"{base_prefix}/{self.service_name}/dashboard/{'/'.join(path_segments)}"
|
||||
elif category == RouteCategory.ANALYTICS:
|
||||
return f"{base_prefix}/{self.service_name}/analytics/{'/'.join(path_segments)}"
|
||||
elif category == RouteCategory.OPERATIONS:
|
||||
return f"{base_prefix}/{self.service_name}/operations/{'/'.join(path_segments)}"
|
||||
|
||||
# Fallback to base
|
||||
return f"{base_prefix}/{self.service_name}/{'/'.join(path_segments)}"
|
||||
|
||||
@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}/.*"
|
||||
211
shared/routing/route_helpers.py
Normal file
211
shared/routing/route_helpers.py
Normal file
@@ -0,0 +1,211 @@
|
||||
"""
|
||||
Helper functions for route building
|
||||
Provides convenience methods for common routing patterns
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
|
||||
def build_base_route(service: str, resource: str, tenant_id: Optional[str] = None) -> str:
|
||||
"""
|
||||
Build a base CRUD route
|
||||
|
||||
Args:
|
||||
service: Service name
|
||||
resource: Resource name
|
||||
tenant_id: Optional tenant ID (if None, uses {tenant_id} placeholder)
|
||||
|
||||
Returns:
|
||||
Complete route path
|
||||
|
||||
Example:
|
||||
build_base_route('inventory', 'ingredients')
|
||||
# Returns: '/api/v1/tenants/{tenant_id}/inventory/ingredients'
|
||||
|
||||
build_base_route('inventory', 'ingredients', 'uuid-123')
|
||||
# Returns: '/api/v1/tenants/uuid-123/inventory/ingredients'
|
||||
"""
|
||||
tenant_part = tenant_id if tenant_id else "{tenant_id}"
|
||||
return f"/api/v1/tenants/{tenant_part}/{service}/{resource}"
|
||||
|
||||
|
||||
def build_dashboard_route(service: str, operation: str, tenant_id: Optional[str] = None) -> str:
|
||||
"""
|
||||
Build a dashboard route
|
||||
|
||||
Args:
|
||||
service: Service name
|
||||
operation: Dashboard operation
|
||||
tenant_id: Optional tenant ID
|
||||
|
||||
Returns:
|
||||
Complete route path
|
||||
|
||||
Example:
|
||||
build_dashboard_route('production', 'summary')
|
||||
# Returns: '/api/v1/tenants/{tenant_id}/production/dashboard/summary'
|
||||
"""
|
||||
tenant_part = tenant_id if tenant_id else "{tenant_id}"
|
||||
return f"/api/v1/tenants/{tenant_part}/{service}/dashboard/{operation}"
|
||||
|
||||
|
||||
def build_analytics_route(service: str, operation: str, tenant_id: Optional[str] = None) -> str:
|
||||
"""
|
||||
Build an analytics route
|
||||
|
||||
Args:
|
||||
service: Service name
|
||||
operation: Analytics operation
|
||||
tenant_id: Optional tenant ID
|
||||
|
||||
Returns:
|
||||
Complete route path
|
||||
|
||||
Example:
|
||||
build_analytics_route('production', 'equipment-efficiency')
|
||||
# Returns: '/api/v1/tenants/{tenant_id}/production/analytics/equipment-efficiency'
|
||||
"""
|
||||
tenant_part = tenant_id if tenant_id else "{tenant_id}"
|
||||
return f"/api/v1/tenants/{tenant_part}/{service}/analytics/{operation}"
|
||||
|
||||
|
||||
def build_operations_route(service: str, operation: str, tenant_id: Optional[str] = None) -> str:
|
||||
"""
|
||||
Build a service operations route
|
||||
|
||||
Args:
|
||||
service: Service name
|
||||
operation: Operation name
|
||||
tenant_id: Optional tenant ID
|
||||
|
||||
Returns:
|
||||
Complete route path
|
||||
|
||||
Example:
|
||||
build_operations_route('production', 'schedule-batch')
|
||||
# Returns: '/api/v1/tenants/{tenant_id}/production/operations/schedule-batch'
|
||||
"""
|
||||
tenant_part = tenant_id if tenant_id else "{tenant_id}"
|
||||
return f"/api/v1/tenants/{tenant_part}/{service}/operations/{operation}"
|
||||
|
||||
|
||||
def build_resource_detail_route(
|
||||
service: str,
|
||||
resource: str,
|
||||
resource_id: Optional[str] = None,
|
||||
tenant_id: Optional[str] = None,
|
||||
id_param_name: str = "id"
|
||||
) -> str:
|
||||
"""
|
||||
Build a route for individual resource details
|
||||
|
||||
Args:
|
||||
service: Service name
|
||||
resource: Resource name
|
||||
resource_id: Optional resource ID (if None, uses parameter name)
|
||||
tenant_id: Optional tenant ID
|
||||
id_param_name: Name of ID parameter when resource_id is None
|
||||
|
||||
Returns:
|
||||
Complete route path
|
||||
|
||||
Example:
|
||||
build_resource_detail_route('inventory', 'ingredients', id_param_name='ingredient_id')
|
||||
# Returns: '/api/v1/tenants/{tenant_id}/inventory/ingredients/{ingredient_id}'
|
||||
|
||||
build_resource_detail_route('inventory', 'ingredients', 'uuid-456', 'uuid-123')
|
||||
# Returns: '/api/v1/tenants/uuid-123/inventory/ingredients/uuid-456'
|
||||
"""
|
||||
base = build_base_route(service, resource, tenant_id)
|
||||
id_part = resource_id if resource_id else f"{{{id_param_name}}}"
|
||||
return f"{base}/{id_part}"
|
||||
|
||||
|
||||
def build_nested_route(
|
||||
service: str,
|
||||
parent_resource: str,
|
||||
child_resource: str,
|
||||
parent_id: Optional[str] = None,
|
||||
tenant_id: Optional[str] = None,
|
||||
parent_id_param: str = "parent_id"
|
||||
) -> str:
|
||||
"""
|
||||
Build a route for nested resources
|
||||
|
||||
Args:
|
||||
service: Service name
|
||||
parent_resource: Parent resource name
|
||||
child_resource: Child resource name
|
||||
parent_id: Optional parent resource ID
|
||||
tenant_id: Optional tenant ID
|
||||
parent_id_param: Parent ID parameter name when parent_id is None
|
||||
|
||||
Returns:
|
||||
Complete route path
|
||||
|
||||
Example:
|
||||
build_nested_route('inventory', 'ingredients', 'stock', parent_id_param='ingredient_id')
|
||||
# Returns: '/api/v1/tenants/{tenant_id}/inventory/ingredients/{ingredient_id}/stock'
|
||||
"""
|
||||
parent = build_resource_detail_route(service, parent_resource, parent_id, tenant_id, parent_id_param)
|
||||
return f"{parent}/{child_resource}"
|
||||
|
||||
|
||||
def extract_tenant_id_from_route(route_path: str) -> Optional[str]:
|
||||
"""
|
||||
Extract tenant ID from a route path
|
||||
|
||||
Args:
|
||||
route_path: Route path containing tenant ID
|
||||
|
||||
Returns:
|
||||
Tenant ID if found, None otherwise
|
||||
|
||||
Example:
|
||||
extract_tenant_id_from_route('/api/v1/tenants/uuid-123/inventory/ingredients')
|
||||
# Returns: 'uuid-123'
|
||||
"""
|
||||
import re
|
||||
match = re.search(r'/api/v1/tenants/([^/]+)/', route_path)
|
||||
return match.group(1) if match else None
|
||||
|
||||
|
||||
def extract_service_from_route(route_path: str) -> Optional[str]:
|
||||
"""
|
||||
Extract service name from a route path
|
||||
|
||||
Args:
|
||||
route_path: Route path containing service name
|
||||
|
||||
Returns:
|
||||
Service name if found, None otherwise
|
||||
|
||||
Example:
|
||||
extract_service_from_route('/api/v1/tenants/uuid-123/inventory/ingredients')
|
||||
# Returns: 'inventory'
|
||||
"""
|
||||
import re
|
||||
match = re.search(r'/api/v1/tenants/[^/]+/([^/]+)/', route_path)
|
||||
return match.group(1) if match else None
|
||||
|
||||
|
||||
def is_analytics_route(route_path: str) -> bool:
|
||||
"""Check if route is an analytics route"""
|
||||
return '/analytics/' in route_path
|
||||
|
||||
|
||||
def is_dashboard_route(route_path: str) -> bool:
|
||||
"""Check if route is a dashboard route"""
|
||||
return '/dashboard/' in route_path
|
||||
|
||||
|
||||
def is_operations_route(route_path: str) -> bool:
|
||||
"""Check if route is an operations route"""
|
||||
return '/operations/' in route_path
|
||||
|
||||
|
||||
def is_base_crud_route(route_path: str) -> bool:
|
||||
"""Check if route is a base CRUD route"""
|
||||
return not (is_analytics_route(route_path) or
|
||||
is_dashboard_route(route_path) or
|
||||
is_operations_route(route_path))
|
||||
Reference in New Issue
Block a user