yirifi-ops-auth-client 3.2.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,155 @@
1
+ """Flask blueprint for exposing entity references via API.
2
+
3
+ This blueprint exposes the /api/v1/references endpoint that returns
4
+ this microsite's entity definitions for federation discovery.
5
+
6
+ Usage:
7
+ from yirifi_ops_auth.deeplink import setup_deeplinks
8
+
9
+ # In your app factory
10
+ setup_deeplinks(
11
+ app,
12
+ expose_references=True, # Registers this blueprint
13
+ )
14
+
15
+ # Or manually:
16
+ from yirifi_ops_auth.deeplink.blueprint import references_bp
17
+ app.register_blueprint(references_bp)
18
+ """
19
+
20
+ from datetime import datetime
21
+ from flask import Blueprint, jsonify, request, current_app
22
+
23
+ from .registry import (
24
+ get_microsite_config,
25
+ list_entity_types_for_microsite,
26
+ get_entity_definition,
27
+ )
28
+
29
+
30
+ references_bp = Blueprint("deeplink_references", __name__, url_prefix="/api/v1")
31
+
32
+
33
+ @references_bp.route("/references", methods=["GET"])
34
+ def get_references():
35
+ """Expose this microsite's entity definitions for federation.
36
+
37
+ This endpoint allows other microsites to discover what entity types
38
+ this microsite serves and how to construct URLs for them.
39
+
40
+ Query Parameters:
41
+ entity_type (str, optional): Filter to specific entity type
42
+ include_urls (bool, default=true): Include URL mappings in response
43
+
44
+ Returns:
45
+ JSON response with schema:
46
+ {
47
+ "schema_version": "1.0",
48
+ "microsite": {
49
+ "id": "risk",
50
+ "name": "Risk Dashboard",
51
+ "urls": {
52
+ "dev": "http://localhost:5012",
53
+ "uat": "https://risk-uat.ops.yirifi.com",
54
+ "prd": "https://risk.ops.yirifi.com"
55
+ }
56
+ },
57
+ "entities": [
58
+ {
59
+ "type": "risk_item",
60
+ "path": "/items/{id}",
61
+ "description": "Risk management item"
62
+ }
63
+ ],
64
+ "timestamp": "2024-12-23T10:30:00Z"
65
+ }
66
+
67
+ Response Headers:
68
+ Cache-Control: public, max-age=300
69
+ X-Federation-Version: 1.0
70
+ """
71
+ # Get microsite config (set during setup)
72
+ microsite_id = current_app.config.get("DEEPLINK_MICROSITE_ID")
73
+ if not microsite_id:
74
+ return (
75
+ jsonify(
76
+ {
77
+ "error": "microsite_not_configured",
78
+ "message": "This microsite has not configured deep linking. "
79
+ "Call setup_deeplinks() with a microsite parameter.",
80
+ }
81
+ ),
82
+ 500,
83
+ )
84
+
85
+ config = get_microsite_config(microsite_id)
86
+ if not config:
87
+ return (
88
+ jsonify(
89
+ {
90
+ "error": "microsite_not_found",
91
+ "message": f"Microsite '{microsite_id}' not registered in local registry.",
92
+ }
93
+ ),
94
+ 500,
95
+ )
96
+
97
+ # Parse query parameters
98
+ entity_type_filter = request.args.get("entity_type")
99
+ include_urls = request.args.get("include_urls", "true").lower() == "true"
100
+
101
+ # Get entities for this microsite
102
+ entity_types = list_entity_types_for_microsite(microsite_id)
103
+
104
+ if entity_type_filter:
105
+ entity_types = [et for et in entity_types if et == entity_type_filter]
106
+
107
+ entities = []
108
+ for entity_type in entity_types:
109
+ defn = get_entity_definition(entity_type)
110
+ if defn:
111
+ entity_data = {
112
+ "type": defn.entity_type,
113
+ "path": defn.path_template,
114
+ }
115
+ if defn.description:
116
+ entity_data["description"] = defn.description
117
+ entities.append(entity_data)
118
+
119
+ # Build response
120
+ response_data = {
121
+ "schema_version": "1.0",
122
+ "microsite": {
123
+ "id": config.id,
124
+ "name": config.name,
125
+ },
126
+ "entities": entities,
127
+ "timestamp": datetime.utcnow().isoformat() + "Z",
128
+ }
129
+
130
+ if include_urls:
131
+ response_data["microsite"]["urls"] = config.urls
132
+
133
+ # Build response with cache headers
134
+ resp = jsonify(response_data)
135
+ resp.headers["Cache-Control"] = "public, max-age=300"
136
+ resp.headers["X-Federation-Version"] = "1.0"
137
+
138
+ return resp
139
+
140
+
141
+ @references_bp.route("/references/health", methods=["GET"])
142
+ def references_health():
143
+ """Health check for the references endpoint.
144
+
145
+ Returns simple status for monitoring/load balancer health checks.
146
+ """
147
+ microsite_id = current_app.config.get("DEEPLINK_MICROSITE_ID")
148
+
149
+ return jsonify(
150
+ {
151
+ "status": "ok",
152
+ "microsite_id": microsite_id,
153
+ "federation_version": "1.0",
154
+ }
155
+ )
@@ -0,0 +1,156 @@
1
+ """Thread-safe environment configuration using contextvars.
2
+
3
+ Provides isolation for environment settings across threads and async contexts,
4
+ with a context manager for temporary overrides.
5
+ """
6
+
7
+ import os
8
+ from contextvars import ContextVar
9
+ from contextlib import contextmanager
10
+ from typing import Optional, Generator
11
+
12
+
13
+ # Thread-safe environment storage
14
+ _env_var: ContextVar[Optional[str]] = ContextVar('deeplink_env', default=None)
15
+
16
+ # Valid environments
17
+ VALID_ENVIRONMENTS = ("dev", "uat", "prd")
18
+
19
+ # Environment name aliases
20
+ ENV_ALIASES = {
21
+ "development": "dev",
22
+ "local": "dev",
23
+ "staging": "uat",
24
+ "test": "uat",
25
+ "production": "prd",
26
+ "prod": "prd",
27
+ }
28
+
29
+
30
+ class Environment:
31
+ """Thread-safe environment configuration.
32
+
33
+ Usage:
34
+ # Get current environment
35
+ env = Environment.get() # -> 'dev', 'uat', or 'prd'
36
+
37
+ # Set environment
38
+ Environment.set('prd')
39
+
40
+ # Temporary override (for testing)
41
+ with Environment.override('uat'):
42
+ # Inside this block, env is 'uat'
43
+ do_something()
44
+ # Outside, env is restored to previous value
45
+ """
46
+
47
+ VALID = VALID_ENVIRONMENTS
48
+
49
+ @classmethod
50
+ def get(cls) -> str:
51
+ """Get the current environment.
52
+
53
+ Resolution order:
54
+ 1. Explicitly set via Environment.set() (thread-local)
55
+ 2. YIRIFI_ENV environment variable
56
+ 3. FLASK_ENV environment variable
57
+ 4. Default to 'dev'
58
+
59
+ Returns:
60
+ Environment string: 'dev', 'uat', or 'prd'
61
+ """
62
+ # Check thread-local context first
63
+ env = _env_var.get()
64
+ if env is not None:
65
+ return env
66
+
67
+ # Fall back to environment variables
68
+ return cls._from_env_vars()
69
+
70
+ @classmethod
71
+ def _from_env_vars(cls) -> str:
72
+ """Read environment from OS environment variables."""
73
+ env = os.getenv("YIRIFI_ENV") or os.getenv("FLASK_ENV", "dev")
74
+ return cls._normalize(env)
75
+
76
+ @classmethod
77
+ def _normalize(cls, env: str) -> str:
78
+ """Normalize environment name to standard form.
79
+
80
+ Args:
81
+ env: Raw environment string
82
+
83
+ Returns:
84
+ Normalized environment: 'dev', 'uat', or 'prd'
85
+ """
86
+ env = env.lower()
87
+ env = ENV_ALIASES.get(env, env)
88
+ return env if env in VALID_ENVIRONMENTS else "dev"
89
+
90
+ @classmethod
91
+ def set(cls, env: str) -> None:
92
+ """Set the environment for the current context.
93
+
94
+ Args:
95
+ env: Environment string ('dev', 'uat', or 'prd') or alias
96
+
97
+ Raises:
98
+ ValueError: If env is not a valid environment or alias
99
+ """
100
+ env_lower = env.lower()
101
+ # Check if it's a valid environment or a known alias
102
+ if env_lower not in VALID_ENVIRONMENTS and env_lower not in ENV_ALIASES:
103
+ raise ValueError(
104
+ f"Invalid environment: {env}. Must be one of {VALID_ENVIRONMENTS} "
105
+ f"or aliases: {list(ENV_ALIASES.keys())}"
106
+ )
107
+ normalized = cls._normalize(env)
108
+ _env_var.set(normalized)
109
+
110
+ @classmethod
111
+ def reset(cls) -> None:
112
+ """Reset environment to auto-detection (clear explicit setting)."""
113
+ _env_var.set(None)
114
+
115
+ @classmethod
116
+ @contextmanager
117
+ def override(cls, env: str) -> Generator[None, None, None]:
118
+ """Temporarily override the environment.
119
+
120
+ Useful for testing or when you need to resolve links for a different
121
+ environment temporarily.
122
+
123
+ Args:
124
+ env: Environment to use within the context
125
+
126
+ Example:
127
+ Environment.set('dev')
128
+ print(Environment.get()) # 'dev'
129
+
130
+ with Environment.override('prd'):
131
+ print(Environment.get()) # 'prd'
132
+
133
+ print(Environment.get()) # 'dev' (restored)
134
+ """
135
+ normalized = cls._normalize(env)
136
+ if normalized not in VALID_ENVIRONMENTS:
137
+ raise ValueError(
138
+ f"Invalid environment: {env}. Must be one of {VALID_ENVIRONMENTS}"
139
+ )
140
+
141
+ token = _env_var.set(normalized)
142
+ try:
143
+ yield
144
+ finally:
145
+ _env_var.reset(token)
146
+
147
+
148
+ # Convenience functions for backwards compatibility and simpler imports
149
+ def get_environment() -> str:
150
+ """Get the current environment. Alias for Environment.get()."""
151
+ return Environment.get()
152
+
153
+
154
+ def set_environment(env: str) -> None:
155
+ """Set the environment. Alias for Environment.set()."""
156
+ Environment.set(env)