morphml 1.0.0__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.

Potentially problematic release.


This version of morphml might be problematic. Click here for more details.

Files changed (158) hide show
  1. morphml/__init__.py +14 -0
  2. morphml/api/__init__.py +26 -0
  3. morphml/api/app.py +326 -0
  4. morphml/api/auth.py +193 -0
  5. morphml/api/client.py +338 -0
  6. morphml/api/models.py +132 -0
  7. morphml/api/rate_limit.py +192 -0
  8. morphml/benchmarking/__init__.py +36 -0
  9. morphml/benchmarking/comparison.py +430 -0
  10. morphml/benchmarks/__init__.py +56 -0
  11. morphml/benchmarks/comparator.py +409 -0
  12. morphml/benchmarks/datasets.py +280 -0
  13. morphml/benchmarks/metrics.py +199 -0
  14. morphml/benchmarks/openml_suite.py +201 -0
  15. morphml/benchmarks/problems.py +289 -0
  16. morphml/benchmarks/suite.py +318 -0
  17. morphml/cli/__init__.py +5 -0
  18. morphml/cli/commands/experiment.py +329 -0
  19. morphml/cli/main.py +457 -0
  20. morphml/cli/quickstart.py +312 -0
  21. morphml/config.py +278 -0
  22. morphml/constraints/__init__.py +19 -0
  23. morphml/constraints/handler.py +205 -0
  24. morphml/constraints/predicates.py +285 -0
  25. morphml/core/__init__.py +3 -0
  26. morphml/core/crossover.py +449 -0
  27. morphml/core/dsl/README.md +359 -0
  28. morphml/core/dsl/__init__.py +72 -0
  29. morphml/core/dsl/ast_nodes.py +364 -0
  30. morphml/core/dsl/compiler.py +318 -0
  31. morphml/core/dsl/layers.py +368 -0
  32. morphml/core/dsl/lexer.py +336 -0
  33. morphml/core/dsl/parser.py +455 -0
  34. morphml/core/dsl/search_space.py +386 -0
  35. morphml/core/dsl/syntax.py +199 -0
  36. morphml/core/dsl/type_system.py +361 -0
  37. morphml/core/dsl/validator.py +386 -0
  38. morphml/core/graph/__init__.py +40 -0
  39. morphml/core/graph/edge.py +124 -0
  40. morphml/core/graph/graph.py +507 -0
  41. morphml/core/graph/mutations.py +409 -0
  42. morphml/core/graph/node.py +196 -0
  43. morphml/core/graph/serialization.py +361 -0
  44. morphml/core/graph/visualization.py +431 -0
  45. morphml/core/objectives/__init__.py +20 -0
  46. morphml/core/search/__init__.py +33 -0
  47. morphml/core/search/individual.py +252 -0
  48. morphml/core/search/parameters.py +453 -0
  49. morphml/core/search/population.py +375 -0
  50. morphml/core/search/search_engine.py +340 -0
  51. morphml/distributed/__init__.py +76 -0
  52. morphml/distributed/fault_tolerance.py +497 -0
  53. morphml/distributed/health_monitor.py +348 -0
  54. morphml/distributed/master.py +709 -0
  55. morphml/distributed/proto/README.md +224 -0
  56. morphml/distributed/proto/__init__.py +74 -0
  57. morphml/distributed/proto/worker.proto +170 -0
  58. morphml/distributed/proto/worker_pb2.py +79 -0
  59. morphml/distributed/proto/worker_pb2_grpc.py +423 -0
  60. morphml/distributed/resource_manager.py +416 -0
  61. morphml/distributed/scheduler.py +567 -0
  62. morphml/distributed/storage/__init__.py +33 -0
  63. morphml/distributed/storage/artifacts.py +381 -0
  64. morphml/distributed/storage/cache.py +366 -0
  65. morphml/distributed/storage/checkpointing.py +329 -0
  66. morphml/distributed/storage/database.py +459 -0
  67. morphml/distributed/worker.py +549 -0
  68. morphml/evaluation/__init__.py +5 -0
  69. morphml/evaluation/heuristic.py +237 -0
  70. morphml/exceptions.py +55 -0
  71. morphml/execution/__init__.py +5 -0
  72. morphml/execution/local_executor.py +350 -0
  73. morphml/integrations/__init__.py +28 -0
  74. morphml/integrations/jax_adapter.py +206 -0
  75. morphml/integrations/pytorch_adapter.py +530 -0
  76. morphml/integrations/sklearn_adapter.py +206 -0
  77. morphml/integrations/tensorflow_adapter.py +230 -0
  78. morphml/logging_config.py +93 -0
  79. morphml/meta_learning/__init__.py +66 -0
  80. morphml/meta_learning/architecture_similarity.py +277 -0
  81. morphml/meta_learning/experiment_database.py +240 -0
  82. morphml/meta_learning/knowledge_base/__init__.py +19 -0
  83. morphml/meta_learning/knowledge_base/embedder.py +179 -0
  84. morphml/meta_learning/knowledge_base/knowledge_base.py +313 -0
  85. morphml/meta_learning/knowledge_base/meta_features.py +265 -0
  86. morphml/meta_learning/knowledge_base/vector_store.py +271 -0
  87. morphml/meta_learning/predictors/__init__.py +27 -0
  88. morphml/meta_learning/predictors/ensemble.py +221 -0
  89. morphml/meta_learning/predictors/gnn_predictor.py +552 -0
  90. morphml/meta_learning/predictors/learning_curve.py +231 -0
  91. morphml/meta_learning/predictors/proxy_metrics.py +261 -0
  92. morphml/meta_learning/strategy_evolution/__init__.py +27 -0
  93. morphml/meta_learning/strategy_evolution/adaptive_optimizer.py +226 -0
  94. morphml/meta_learning/strategy_evolution/bandit.py +276 -0
  95. morphml/meta_learning/strategy_evolution/portfolio.py +230 -0
  96. morphml/meta_learning/transfer.py +581 -0
  97. morphml/meta_learning/warm_start.py +286 -0
  98. morphml/optimizers/__init__.py +74 -0
  99. morphml/optimizers/adaptive_operators.py +399 -0
  100. morphml/optimizers/bayesian/__init__.py +52 -0
  101. morphml/optimizers/bayesian/acquisition.py +387 -0
  102. morphml/optimizers/bayesian/base.py +319 -0
  103. morphml/optimizers/bayesian/gaussian_process.py +635 -0
  104. morphml/optimizers/bayesian/smac.py +534 -0
  105. morphml/optimizers/bayesian/tpe.py +411 -0
  106. morphml/optimizers/differential_evolution.py +220 -0
  107. morphml/optimizers/evolutionary/__init__.py +61 -0
  108. morphml/optimizers/evolutionary/cma_es.py +416 -0
  109. morphml/optimizers/evolutionary/differential_evolution.py +556 -0
  110. morphml/optimizers/evolutionary/encoding.py +426 -0
  111. morphml/optimizers/evolutionary/particle_swarm.py +449 -0
  112. morphml/optimizers/genetic_algorithm.py +486 -0
  113. morphml/optimizers/gradient_based/__init__.py +22 -0
  114. morphml/optimizers/gradient_based/darts.py +550 -0
  115. morphml/optimizers/gradient_based/enas.py +585 -0
  116. morphml/optimizers/gradient_based/operations.py +474 -0
  117. morphml/optimizers/gradient_based/utils.py +601 -0
  118. morphml/optimizers/hill_climbing.py +169 -0
  119. morphml/optimizers/multi_objective/__init__.py +56 -0
  120. morphml/optimizers/multi_objective/indicators.py +504 -0
  121. morphml/optimizers/multi_objective/nsga2.py +647 -0
  122. morphml/optimizers/multi_objective/visualization.py +427 -0
  123. morphml/optimizers/nsga2.py +308 -0
  124. morphml/optimizers/random_search.py +172 -0
  125. morphml/optimizers/simulated_annealing.py +181 -0
  126. morphml/plugins/__init__.py +35 -0
  127. morphml/plugins/custom_evaluator_example.py +81 -0
  128. morphml/plugins/custom_optimizer_example.py +63 -0
  129. morphml/plugins/plugin_system.py +454 -0
  130. morphml/reports/__init__.py +30 -0
  131. morphml/reports/generator.py +362 -0
  132. morphml/tracking/__init__.py +7 -0
  133. morphml/tracking/experiment.py +309 -0
  134. morphml/tracking/logger.py +301 -0
  135. morphml/tracking/reporter.py +357 -0
  136. morphml/utils/__init__.py +6 -0
  137. morphml/utils/checkpoint.py +189 -0
  138. morphml/utils/comparison.py +390 -0
  139. morphml/utils/export.py +407 -0
  140. morphml/utils/progress.py +392 -0
  141. morphml/utils/validation.py +392 -0
  142. morphml/version.py +7 -0
  143. morphml/visualization/__init__.py +50 -0
  144. morphml/visualization/analytics.py +423 -0
  145. morphml/visualization/architecture_diagrams.py +353 -0
  146. morphml/visualization/architecture_plot.py +223 -0
  147. morphml/visualization/convergence_plot.py +174 -0
  148. morphml/visualization/crossover_viz.py +386 -0
  149. morphml/visualization/graph_viz.py +338 -0
  150. morphml/visualization/pareto_plot.py +149 -0
  151. morphml/visualization/plotly_dashboards.py +422 -0
  152. morphml/visualization/population.py +309 -0
  153. morphml/visualization/progress.py +260 -0
  154. morphml-1.0.0.dist-info/METADATA +434 -0
  155. morphml-1.0.0.dist-info/RECORD +158 -0
  156. morphml-1.0.0.dist-info/WHEEL +4 -0
  157. morphml-1.0.0.dist-info/entry_points.txt +3 -0
  158. morphml-1.0.0.dist-info/licenses/LICENSE +21 -0
morphml/__init__.py ADDED
@@ -0,0 +1,14 @@
1
+ """
2
+ MorphML: Production-grade Neural Architecture Search
3
+
4
+ A comprehensive framework for automated neural architecture search with
5
+ distributed optimization, meta-learning, and multi-objective capabilities.
6
+
7
+ Author: Eshan Roy <eshanized@proton.me>
8
+ Organization: TONMOY INFRASTRUCTURE & VISION
9
+ Repository: https://github.com/TIVerse/MorphML
10
+ """
11
+
12
+ from morphml.version import __version__
13
+
14
+ __all__ = ["__version__"]
@@ -0,0 +1,26 @@
1
+ """REST API for MorphML.
2
+
3
+ Provides programmatic access to MorphML functionality via HTTP endpoints.
4
+
5
+ Example:
6
+ # Start server
7
+ morphml api --port 8000
8
+
9
+ # Or programmatically
10
+ from morphml.api import create_app
11
+ app = create_app()
12
+ """
13
+
14
+ from morphml.api.app import create_app
15
+ from morphml.api.models import (
16
+ ArchitectureResponse,
17
+ ExperimentCreate,
18
+ ExperimentResponse,
19
+ )
20
+
21
+ __all__ = [
22
+ "create_app",
23
+ "ExperimentCreate",
24
+ "ExperimentResponse",
25
+ "ArchitectureResponse",
26
+ ]
morphml/api/app.py ADDED
@@ -0,0 +1,326 @@
1
+ """FastAPI application for MorphML REST API.
2
+
3
+ This module provides the main FastAPI application with all endpoints.
4
+ """
5
+
6
+ import uuid
7
+ from datetime import datetime
8
+ from typing import List, Optional
9
+
10
+ try:
11
+ from fastapi import FastAPI, HTTPException, status
12
+ from fastapi.middleware.cors import CORSMiddleware
13
+
14
+ FASTAPI_AVAILABLE = True
15
+ except ImportError:
16
+ FASTAPI_AVAILABLE = False
17
+ FastAPI = None
18
+
19
+ from morphml.api.models import (
20
+ ArchitectureResponse,
21
+ ExperimentCreate,
22
+ ExperimentResponse,
23
+ HealthResponse,
24
+ OptimizerInfo,
25
+ )
26
+ from morphml.logging_config import get_logger
27
+ from morphml.version import __version__
28
+
29
+ logger = get_logger(__name__)
30
+
31
+ # In-memory storage (replace with database in production)
32
+ experiments_db = {}
33
+ architectures_db = {}
34
+
35
+
36
+ def create_app() -> FastAPI:
37
+ """
38
+ Create and configure FastAPI application.
39
+
40
+ Returns:
41
+ Configured FastAPI app
42
+
43
+ Example:
44
+ >>> app = create_app()
45
+ >>> # Run with: uvicorn morphml.api.app:app --reload
46
+ """
47
+ if not FASTAPI_AVAILABLE:
48
+ raise ImportError(
49
+ "FastAPI is required for the REST API. "
50
+ "Install with: pip install 'morphml[api]' or pip install fastapi uvicorn"
51
+ )
52
+
53
+ app = FastAPI(
54
+ title="MorphML API",
55
+ description="REST API for Neural Architecture Search with MorphML",
56
+ version=__version__,
57
+ docs_url="/docs",
58
+ redoc_url="/redoc",
59
+ )
60
+
61
+ # CORS middleware
62
+ app.add_middleware(
63
+ CORSMiddleware,
64
+ allow_origins=["*"], # Configure appropriately for production
65
+ allow_credentials=True,
66
+ allow_methods=["*"],
67
+ allow_headers=["*"],
68
+ )
69
+
70
+ # Health check endpoint
71
+ @app.get("/health", response_model=HealthResponse, tags=["Health"])
72
+ async def health_check():
73
+ """Check API health status."""
74
+ return HealthResponse(status="healthy", version=__version__, timestamp=datetime.now())
75
+
76
+ # Experiment endpoints
77
+ @app.post(
78
+ "/api/v1/experiments",
79
+ response_model=ExperimentResponse,
80
+ status_code=status.HTTP_201_CREATED,
81
+ tags=["Experiments"],
82
+ )
83
+ async def create_experiment(experiment: ExperimentCreate):
84
+ """
85
+ Create a new NAS experiment.
86
+
87
+ Args:
88
+ experiment: Experiment configuration
89
+
90
+ Returns:
91
+ Created experiment details
92
+ """
93
+ experiment_id = f"exp_{uuid.uuid4().hex[:8]}"
94
+
95
+ exp_data = ExperimentResponse(
96
+ id=experiment_id,
97
+ name=experiment.name,
98
+ status="pending",
99
+ created_at=datetime.now(),
100
+ best_fitness=None,
101
+ generations_completed=0,
102
+ total_generations=experiment.optimizer_config.get("num_generations", 50),
103
+ )
104
+
105
+ experiments_db[experiment_id] = {"response": exp_data, "config": experiment.dict()}
106
+
107
+ logger.info(f"Created experiment: {experiment_id}")
108
+ return exp_data
109
+
110
+ @app.get("/api/v1/experiments", response_model=List[ExperimentResponse], tags=["Experiments"])
111
+ async def list_experiments(
112
+ status_filter: Optional[str] = None, limit: int = 100, offset: int = 0
113
+ ):
114
+ """
115
+ List all experiments.
116
+
117
+ Args:
118
+ status_filter: Filter by status (pending, running, completed, failed)
119
+ limit: Maximum number of results
120
+ offset: Offset for pagination
121
+
122
+ Returns:
123
+ List of experiments
124
+ """
125
+ experiments = [exp["response"] for exp in experiments_db.values()]
126
+
127
+ if status_filter:
128
+ experiments = [exp for exp in experiments if exp.status == status_filter]
129
+
130
+ return experiments[offset : offset + limit]
131
+
132
+ @app.get(
133
+ "/api/v1/experiments/{experiment_id}",
134
+ response_model=ExperimentResponse,
135
+ tags=["Experiments"],
136
+ )
137
+ async def get_experiment(experiment_id: str):
138
+ """
139
+ Get experiment details.
140
+
141
+ Args:
142
+ experiment_id: Experiment ID
143
+
144
+ Returns:
145
+ Experiment details
146
+ """
147
+ if experiment_id not in experiments_db:
148
+ raise HTTPException(
149
+ status_code=status.HTTP_404_NOT_FOUND,
150
+ detail=f"Experiment {experiment_id} not found",
151
+ )
152
+
153
+ return experiments_db[experiment_id]["response"]
154
+
155
+ @app.post(
156
+ "/api/v1/experiments/{experiment_id}/start",
157
+ response_model=ExperimentResponse,
158
+ tags=["Experiments"],
159
+ )
160
+ async def start_experiment(experiment_id: str):
161
+ """
162
+ Start an experiment.
163
+
164
+ Args:
165
+ experiment_id: Experiment ID
166
+
167
+ Returns:
168
+ Updated experiment details
169
+ """
170
+ if experiment_id not in experiments_db:
171
+ raise HTTPException(
172
+ status_code=status.HTTP_404_NOT_FOUND,
173
+ detail=f"Experiment {experiment_id} not found",
174
+ )
175
+
176
+ exp_data = experiments_db[experiment_id]["response"]
177
+ exp_data.status = "running"
178
+ exp_data.started_at = datetime.now()
179
+
180
+ logger.info(f"Started experiment: {experiment_id}")
181
+ return exp_data
182
+
183
+ @app.post(
184
+ "/api/v1/experiments/{experiment_id}/stop",
185
+ response_model=ExperimentResponse,
186
+ tags=["Experiments"],
187
+ )
188
+ async def stop_experiment(experiment_id: str):
189
+ """
190
+ Stop a running experiment.
191
+
192
+ Args:
193
+ experiment_id: Experiment ID
194
+
195
+ Returns:
196
+ Updated experiment details
197
+ """
198
+ if experiment_id not in experiments_db:
199
+ raise HTTPException(
200
+ status_code=status.HTTP_404_NOT_FOUND,
201
+ detail=f"Experiment {experiment_id} not found",
202
+ )
203
+
204
+ exp_data = experiments_db[experiment_id]["response"]
205
+ if exp_data.status == "running":
206
+ exp_data.status = "stopped"
207
+ exp_data.completed_at = datetime.now()
208
+
209
+ logger.info(f"Stopped experiment: {experiment_id}")
210
+ return exp_data
211
+
212
+ @app.delete(
213
+ "/api/v1/experiments/{experiment_id}",
214
+ status_code=status.HTTP_204_NO_CONTENT,
215
+ tags=["Experiments"],
216
+ )
217
+ async def delete_experiment(experiment_id: str):
218
+ """
219
+ Delete an experiment.
220
+
221
+ Args:
222
+ experiment_id: Experiment ID
223
+ """
224
+ if experiment_id not in experiments_db:
225
+ raise HTTPException(
226
+ status_code=status.HTTP_404_NOT_FOUND,
227
+ detail=f"Experiment {experiment_id} not found",
228
+ )
229
+
230
+ del experiments_db[experiment_id]
231
+ logger.info(f"Deleted experiment: {experiment_id}")
232
+
233
+ # Architecture endpoints
234
+ @app.get(
235
+ "/api/v1/architectures", response_model=List[ArchitectureResponse], tags=["Architectures"]
236
+ )
237
+ async def list_architectures(
238
+ experiment_id: Optional[str] = None, limit: int = 100, offset: int = 0
239
+ ):
240
+ """
241
+ List architectures.
242
+
243
+ Args:
244
+ experiment_id: Filter by experiment ID
245
+ limit: Maximum number of results
246
+ offset: Offset for pagination
247
+
248
+ Returns:
249
+ List of architectures
250
+ """
251
+ architectures = list(architectures_db.values())
252
+
253
+ if experiment_id:
254
+ architectures = [arch for arch in architectures if arch.experiment_id == experiment_id]
255
+
256
+ return architectures[offset : offset + limit]
257
+
258
+ @app.get(
259
+ "/api/v1/architectures/{architecture_id}",
260
+ response_model=ArchitectureResponse,
261
+ tags=["Architectures"],
262
+ )
263
+ async def get_architecture(architecture_id: str):
264
+ """
265
+ Get architecture details.
266
+
267
+ Args:
268
+ architecture_id: Architecture ID
269
+
270
+ Returns:
271
+ Architecture details
272
+ """
273
+ if architecture_id not in architectures_db:
274
+ raise HTTPException(
275
+ status_code=status.HTTP_404_NOT_FOUND,
276
+ detail=f"Architecture {architecture_id} not found",
277
+ )
278
+
279
+ return architectures_db[architecture_id]
280
+
281
+ # Optimizer info endpoint
282
+ @app.get("/api/v1/optimizers", response_model=List[OptimizerInfo], tags=["Optimizers"])
283
+ async def list_optimizers():
284
+ """
285
+ List available optimizers.
286
+
287
+ Returns:
288
+ List of optimizer information
289
+ """
290
+ return [
291
+ OptimizerInfo(
292
+ name="GeneticAlgorithm",
293
+ type="evolutionary",
294
+ description="Genetic algorithm for neural architecture search",
295
+ parameters={
296
+ "population_size": {"type": "int", "default": 20},
297
+ "num_generations": {"type": "int", "default": 50},
298
+ "mutation_rate": {"type": "float", "default": 0.2},
299
+ "crossover_rate": {"type": "float", "default": 0.8},
300
+ },
301
+ ),
302
+ OptimizerInfo(
303
+ name="RandomSearch",
304
+ type="random",
305
+ description="Random search baseline",
306
+ parameters={
307
+ "num_samples": {"type": "int", "default": 100},
308
+ },
309
+ ),
310
+ OptimizerInfo(
311
+ name="HillClimbing",
312
+ type="local_search",
313
+ description="Hill climbing local search",
314
+ parameters={
315
+ "max_iterations": {"type": "int", "default": 100},
316
+ "patience": {"type": "int", "default": 10},
317
+ },
318
+ ),
319
+ ]
320
+
321
+ logger.info("FastAPI application created")
322
+ return app
323
+
324
+
325
+ # Create app instance for uvicorn
326
+ app = create_app() if FASTAPI_AVAILABLE else None
morphml/api/auth.py ADDED
@@ -0,0 +1,193 @@
1
+ """Authentication utilities for MorphML API.
2
+
3
+ Provides JWT-based authentication for the REST API.
4
+
5
+ Example:
6
+ >>> from morphml.api.auth import create_access_token, verify_token
7
+ >>> token = create_access_token({"sub": "user@example.com"})
8
+ >>> payload = verify_token(token)
9
+ """
10
+
11
+ from datetime import datetime, timedelta
12
+ from typing import Any, Dict, Optional
13
+
14
+ try:
15
+ from jose import JWTError, jwt
16
+ from passlib.context import CryptContext
17
+
18
+ JOSE_AVAILABLE = True
19
+ except ImportError:
20
+ JOSE_AVAILABLE = False
21
+ jwt = None
22
+ CryptContext = None
23
+
24
+ from morphml.logging_config import get_logger
25
+
26
+ logger = get_logger(__name__)
27
+
28
+ # Configuration
29
+ SECRET_KEY = "your-secret-key-change-in-production" # Change this!
30
+ ALGORITHM = "HS256"
31
+ ACCESS_TOKEN_EXPIRE_MINUTES = 30
32
+
33
+ # Password hashing
34
+ if JOSE_AVAILABLE:
35
+ pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
36
+ else:
37
+ pwd_context = None
38
+
39
+
40
+ def verify_password(plain_password: str, hashed_password: str) -> bool:
41
+ """
42
+ Verify a password against its hash.
43
+
44
+ Args:
45
+ plain_password: Plain text password
46
+ hashed_password: Hashed password
47
+
48
+ Returns:
49
+ True if password matches
50
+ """
51
+ if not JOSE_AVAILABLE:
52
+ raise ImportError("python-jose and passlib required for authentication")
53
+
54
+ return pwd_context.verify(plain_password, hashed_password)
55
+
56
+
57
+ def get_password_hash(password: str) -> str:
58
+ """
59
+ Hash a password.
60
+
61
+ Args:
62
+ password: Plain text password
63
+
64
+ Returns:
65
+ Hashed password
66
+ """
67
+ if not JOSE_AVAILABLE:
68
+ raise ImportError("python-jose and passlib required for authentication")
69
+
70
+ return pwd_context.hash(password)
71
+
72
+
73
+ def create_access_token(data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str:
74
+ """
75
+ Create JWT access token.
76
+
77
+ Args:
78
+ data: Data to encode in token
79
+ expires_delta: Optional expiration time
80
+
81
+ Returns:
82
+ JWT token string
83
+
84
+ Example:
85
+ >>> token = create_access_token({"sub": "user@example.com"})
86
+ """
87
+ if not JOSE_AVAILABLE:
88
+ raise ImportError("python-jose required for authentication")
89
+
90
+ to_encode = data.copy()
91
+
92
+ if expires_delta:
93
+ expire = datetime.utcnow() + expires_delta
94
+ else:
95
+ expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
96
+
97
+ to_encode.update({"exp": expire})
98
+
99
+ encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
100
+
101
+ return encoded_jwt
102
+
103
+
104
+ def verify_token(token: str) -> Optional[Dict[str, Any]]:
105
+ """
106
+ Verify and decode JWT token.
107
+
108
+ Args:
109
+ token: JWT token string
110
+
111
+ Returns:
112
+ Decoded payload or None if invalid
113
+
114
+ Example:
115
+ >>> payload = verify_token(token)
116
+ >>> if payload:
117
+ ... user_email = payload.get("sub")
118
+ """
119
+ if not JOSE_AVAILABLE:
120
+ raise ImportError("python-jose required for authentication")
121
+
122
+ try:
123
+ payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
124
+ return payload
125
+ except JWTError as e:
126
+ logger.warning(f"Token verification failed: {e}")
127
+ return None
128
+
129
+
130
+ # Simple in-memory user store (replace with database in production)
131
+ fake_users_db = {
132
+ "admin@morphml.com": {
133
+ "username": "admin@morphml.com",
134
+ "full_name": "Admin User",
135
+ "email": "admin@morphml.com",
136
+ "hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW", # "secret"
137
+ "disabled": False,
138
+ }
139
+ }
140
+
141
+
142
+ def authenticate_user(username: str, password: str) -> Optional[Dict[str, Any]]:
143
+ """
144
+ Authenticate user with username and password.
145
+
146
+ Args:
147
+ username: Username (email)
148
+ password: Plain text password
149
+
150
+ Returns:
151
+ User dict if authenticated, None otherwise
152
+ """
153
+ user = fake_users_db.get(username)
154
+
155
+ if not user:
156
+ return None
157
+
158
+ if not verify_password(password, user["hashed_password"]):
159
+ return None
160
+
161
+ return user
162
+
163
+
164
+ def create_user(username: str, password: str, full_name: str = "") -> Dict[str, Any]:
165
+ """
166
+ Create a new user.
167
+
168
+ Args:
169
+ username: Username (email)
170
+ password: Plain text password
171
+ full_name: Full name
172
+
173
+ Returns:
174
+ Created user dict
175
+ """
176
+ if username in fake_users_db:
177
+ raise ValueError(f"User {username} already exists")
178
+
179
+ hashed_password = get_password_hash(password)
180
+
181
+ user = {
182
+ "username": username,
183
+ "full_name": full_name,
184
+ "email": username,
185
+ "hashed_password": hashed_password,
186
+ "disabled": False,
187
+ }
188
+
189
+ fake_users_db[username] = user
190
+
191
+ logger.info(f"Created user: {username}")
192
+
193
+ return user