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.
- morphml/__init__.py +14 -0
- morphml/api/__init__.py +26 -0
- morphml/api/app.py +326 -0
- morphml/api/auth.py +193 -0
- morphml/api/client.py +338 -0
- morphml/api/models.py +132 -0
- morphml/api/rate_limit.py +192 -0
- morphml/benchmarking/__init__.py +36 -0
- morphml/benchmarking/comparison.py +430 -0
- morphml/benchmarks/__init__.py +56 -0
- morphml/benchmarks/comparator.py +409 -0
- morphml/benchmarks/datasets.py +280 -0
- morphml/benchmarks/metrics.py +199 -0
- morphml/benchmarks/openml_suite.py +201 -0
- morphml/benchmarks/problems.py +289 -0
- morphml/benchmarks/suite.py +318 -0
- morphml/cli/__init__.py +5 -0
- morphml/cli/commands/experiment.py +329 -0
- morphml/cli/main.py +457 -0
- morphml/cli/quickstart.py +312 -0
- morphml/config.py +278 -0
- morphml/constraints/__init__.py +19 -0
- morphml/constraints/handler.py +205 -0
- morphml/constraints/predicates.py +285 -0
- morphml/core/__init__.py +3 -0
- morphml/core/crossover.py +449 -0
- morphml/core/dsl/README.md +359 -0
- morphml/core/dsl/__init__.py +72 -0
- morphml/core/dsl/ast_nodes.py +364 -0
- morphml/core/dsl/compiler.py +318 -0
- morphml/core/dsl/layers.py +368 -0
- morphml/core/dsl/lexer.py +336 -0
- morphml/core/dsl/parser.py +455 -0
- morphml/core/dsl/search_space.py +386 -0
- morphml/core/dsl/syntax.py +199 -0
- morphml/core/dsl/type_system.py +361 -0
- morphml/core/dsl/validator.py +386 -0
- morphml/core/graph/__init__.py +40 -0
- morphml/core/graph/edge.py +124 -0
- morphml/core/graph/graph.py +507 -0
- morphml/core/graph/mutations.py +409 -0
- morphml/core/graph/node.py +196 -0
- morphml/core/graph/serialization.py +361 -0
- morphml/core/graph/visualization.py +431 -0
- morphml/core/objectives/__init__.py +20 -0
- morphml/core/search/__init__.py +33 -0
- morphml/core/search/individual.py +252 -0
- morphml/core/search/parameters.py +453 -0
- morphml/core/search/population.py +375 -0
- morphml/core/search/search_engine.py +340 -0
- morphml/distributed/__init__.py +76 -0
- morphml/distributed/fault_tolerance.py +497 -0
- morphml/distributed/health_monitor.py +348 -0
- morphml/distributed/master.py +709 -0
- morphml/distributed/proto/README.md +224 -0
- morphml/distributed/proto/__init__.py +74 -0
- morphml/distributed/proto/worker.proto +170 -0
- morphml/distributed/proto/worker_pb2.py +79 -0
- morphml/distributed/proto/worker_pb2_grpc.py +423 -0
- morphml/distributed/resource_manager.py +416 -0
- morphml/distributed/scheduler.py +567 -0
- morphml/distributed/storage/__init__.py +33 -0
- morphml/distributed/storage/artifacts.py +381 -0
- morphml/distributed/storage/cache.py +366 -0
- morphml/distributed/storage/checkpointing.py +329 -0
- morphml/distributed/storage/database.py +459 -0
- morphml/distributed/worker.py +549 -0
- morphml/evaluation/__init__.py +5 -0
- morphml/evaluation/heuristic.py +237 -0
- morphml/exceptions.py +55 -0
- morphml/execution/__init__.py +5 -0
- morphml/execution/local_executor.py +350 -0
- morphml/integrations/__init__.py +28 -0
- morphml/integrations/jax_adapter.py +206 -0
- morphml/integrations/pytorch_adapter.py +530 -0
- morphml/integrations/sklearn_adapter.py +206 -0
- morphml/integrations/tensorflow_adapter.py +230 -0
- morphml/logging_config.py +93 -0
- morphml/meta_learning/__init__.py +66 -0
- morphml/meta_learning/architecture_similarity.py +277 -0
- morphml/meta_learning/experiment_database.py +240 -0
- morphml/meta_learning/knowledge_base/__init__.py +19 -0
- morphml/meta_learning/knowledge_base/embedder.py +179 -0
- morphml/meta_learning/knowledge_base/knowledge_base.py +313 -0
- morphml/meta_learning/knowledge_base/meta_features.py +265 -0
- morphml/meta_learning/knowledge_base/vector_store.py +271 -0
- morphml/meta_learning/predictors/__init__.py +27 -0
- morphml/meta_learning/predictors/ensemble.py +221 -0
- morphml/meta_learning/predictors/gnn_predictor.py +552 -0
- morphml/meta_learning/predictors/learning_curve.py +231 -0
- morphml/meta_learning/predictors/proxy_metrics.py +261 -0
- morphml/meta_learning/strategy_evolution/__init__.py +27 -0
- morphml/meta_learning/strategy_evolution/adaptive_optimizer.py +226 -0
- morphml/meta_learning/strategy_evolution/bandit.py +276 -0
- morphml/meta_learning/strategy_evolution/portfolio.py +230 -0
- morphml/meta_learning/transfer.py +581 -0
- morphml/meta_learning/warm_start.py +286 -0
- morphml/optimizers/__init__.py +74 -0
- morphml/optimizers/adaptive_operators.py +399 -0
- morphml/optimizers/bayesian/__init__.py +52 -0
- morphml/optimizers/bayesian/acquisition.py +387 -0
- morphml/optimizers/bayesian/base.py +319 -0
- morphml/optimizers/bayesian/gaussian_process.py +635 -0
- morphml/optimizers/bayesian/smac.py +534 -0
- morphml/optimizers/bayesian/tpe.py +411 -0
- morphml/optimizers/differential_evolution.py +220 -0
- morphml/optimizers/evolutionary/__init__.py +61 -0
- morphml/optimizers/evolutionary/cma_es.py +416 -0
- morphml/optimizers/evolutionary/differential_evolution.py +556 -0
- morphml/optimizers/evolutionary/encoding.py +426 -0
- morphml/optimizers/evolutionary/particle_swarm.py +449 -0
- morphml/optimizers/genetic_algorithm.py +486 -0
- morphml/optimizers/gradient_based/__init__.py +22 -0
- morphml/optimizers/gradient_based/darts.py +550 -0
- morphml/optimizers/gradient_based/enas.py +585 -0
- morphml/optimizers/gradient_based/operations.py +474 -0
- morphml/optimizers/gradient_based/utils.py +601 -0
- morphml/optimizers/hill_climbing.py +169 -0
- morphml/optimizers/multi_objective/__init__.py +56 -0
- morphml/optimizers/multi_objective/indicators.py +504 -0
- morphml/optimizers/multi_objective/nsga2.py +647 -0
- morphml/optimizers/multi_objective/visualization.py +427 -0
- morphml/optimizers/nsga2.py +308 -0
- morphml/optimizers/random_search.py +172 -0
- morphml/optimizers/simulated_annealing.py +181 -0
- morphml/plugins/__init__.py +35 -0
- morphml/plugins/custom_evaluator_example.py +81 -0
- morphml/plugins/custom_optimizer_example.py +63 -0
- morphml/plugins/plugin_system.py +454 -0
- morphml/reports/__init__.py +30 -0
- morphml/reports/generator.py +362 -0
- morphml/tracking/__init__.py +7 -0
- morphml/tracking/experiment.py +309 -0
- morphml/tracking/logger.py +301 -0
- morphml/tracking/reporter.py +357 -0
- morphml/utils/__init__.py +6 -0
- morphml/utils/checkpoint.py +189 -0
- morphml/utils/comparison.py +390 -0
- morphml/utils/export.py +407 -0
- morphml/utils/progress.py +392 -0
- morphml/utils/validation.py +392 -0
- morphml/version.py +7 -0
- morphml/visualization/__init__.py +50 -0
- morphml/visualization/analytics.py +423 -0
- morphml/visualization/architecture_diagrams.py +353 -0
- morphml/visualization/architecture_plot.py +223 -0
- morphml/visualization/convergence_plot.py +174 -0
- morphml/visualization/crossover_viz.py +386 -0
- morphml/visualization/graph_viz.py +338 -0
- morphml/visualization/pareto_plot.py +149 -0
- morphml/visualization/plotly_dashboards.py +422 -0
- morphml/visualization/population.py +309 -0
- morphml/visualization/progress.py +260 -0
- morphml-1.0.0.dist-info/METADATA +434 -0
- morphml-1.0.0.dist-info/RECORD +158 -0
- morphml-1.0.0.dist-info/WHEEL +4 -0
- morphml-1.0.0.dist-info/entry_points.txt +3 -0
- 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__"]
|
morphml/api/__init__.py
ADDED
|
@@ -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
|