factorforge-cds 3.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.
Files changed (52) hide show
  1. factorforge/__init__.py +19 -0
  2. factorforge/__main__.py +8 -0
  3. factorforge/cli/__init__.py +5 -0
  4. factorforge/cli/legacy_cli.py +157 -0
  5. factorforge/cli/main.py +305 -0
  6. factorforge/core/interfaces/__init__.py +7 -0
  7. factorforge/core/interfaces/exporter.py +13 -0
  8. factorforge/core/interfaces/optimizer.py +85 -0
  9. factorforge/core/interfaces/validator.py +9 -0
  10. factorforge/database.py +150 -0
  11. factorforge/engines/__init__.py +60 -0
  12. factorforge/engines/ml/__init__.py +0 -0
  13. factorforge/engines/ml/plant_optimizer.py +325 -0
  14. factorforge/engines/registry.py +141 -0
  15. factorforge/engines/v1_archived/__init__.py +15 -0
  16. factorforge/engines/v2/__init__.py +13 -0
  17. factorforge/engines/v2/codon_table_builder.py +107 -0
  18. factorforge/engines/v2/construct_builder.py +403 -0
  19. factorforge/engines/v2/exporter.py +455 -0
  20. factorforge/engines/v2/optimizer.py +190 -0
  21. factorforge/engines/v2/pipeline.py +275 -0
  22. factorforge/engines/v2/rules/__init__.py +3 -0
  23. factorforge/engines/v2/rules/domesticator.py +403 -0
  24. factorforge/engines/v2/rules/reverse_translator.py +765 -0
  25. factorforge/engines/v2/rules/rule_engine.py +867 -0
  26. factorforge/engines/v2/scoring.py +232 -0
  27. factorforge/engines/v2/utils.py +231 -0
  28. factorforge/engines/v2/validator.py +383 -0
  29. factorforge/engines/v3/__init__.py +12 -0
  30. factorforge/engines/v3/explain.py +119 -0
  31. factorforge/engines/v3/inference/__init__.py +6 -0
  32. factorforge/engines/v3/inference/constrained_decoder.py +80 -0
  33. factorforge/engines/v3/inference/v2_adapter.py +72 -0
  34. factorforge/engines/v3/metrics.py +145 -0
  35. factorforge/engines/v3/modeling_bart_decoder.py +127 -0
  36. factorforge/engines/v3/pipeline.py +192 -0
  37. factorforge/engines/v3/synonym_mask.py +61 -0
  38. factorforge/engines/v3/tokenizer.py +192 -0
  39. factorforge/ml/__init__.py +33 -0
  40. factorforge/ml/feasibility.py +199 -0
  41. factorforge/ml/metrics.py +295 -0
  42. factorforge/utils/__init__.py +31 -0
  43. factorforge/utils/construct_id.py +8 -0
  44. factorforge/utils/exceptions.py +32 -0
  45. factorforge/utils/sequence_validator.py +189 -0
  46. factorforge/utils/validation.py +104 -0
  47. factorforge_cds-3.0.0.dist-info/METADATA +475 -0
  48. factorforge_cds-3.0.0.dist-info/RECORD +52 -0
  49. factorforge_cds-3.0.0.dist-info/WHEEL +5 -0
  50. factorforge_cds-3.0.0.dist-info/entry_points.txt +2 -0
  51. factorforge_cds-3.0.0.dist-info/licenses/LICENSE +201 -0
  52. factorforge_cds-3.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,19 @@
1
+ """
2
+ FactorForge - Codon Optimization Platform
3
+
4
+ v1_archived: Rule-based v1 (Archived)
5
+ v2: Rule-based (Production) — engine version 3.0.0
6
+ v3: ML engine / v3-alpha (ESM2 + BART, in development)
7
+ """
8
+
9
+ __version__ = "3.0.0"
10
+ __author__ = "Eijex"
11
+
12
+ # Auto-register engines (safe when running from source tree)
13
+ try:
14
+ from .engines import EngineRegistry, register_builtin_engines
15
+
16
+ register_builtin_engines()
17
+ __all__ = ["EngineRegistry"]
18
+ except Exception:
19
+ __all__ = ["__version__"]
@@ -0,0 +1,8 @@
1
+ """CLI Entry Point"""
2
+
3
+ import sys
4
+
5
+ from factorforge.cli.main import cli
6
+
7
+ if __name__ == "__main__":
8
+ sys.exit(cli())
@@ -0,0 +1,5 @@
1
+ """CLI module"""
2
+
3
+ from .main import cli
4
+
5
+ __all__ = ["cli"]
@@ -0,0 +1,157 @@
1
+ """
2
+ FactorForge Command Line Interface
3
+ For Linux servers and automation
4
+ """
5
+
6
+ import argparse
7
+ import json
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ # Add src root to path
12
+ src_root = next(p for p in Path(__file__).resolve().parents if p.name == "src")
13
+ sys.path.insert(0, str(src_root))
14
+
15
+ def main():
16
+ parser = argparse.ArgumentParser(description="FactorForge - AI-Powered Codon Optimization")
17
+
18
+ parser.add_argument("input", help="Input DNA sequence or FASTA file")
19
+
20
+ parser.add_argument("-o", "--output", help="Output file (JSON format)", default=None)
21
+
22
+ parser.add_argument(
23
+ "-m", "--model", help="Model checkpoint path", default="outputs/checkpoints/phase3"
24
+ )
25
+
26
+ parser.add_argument("--batch", action="store_true", help="Process as FASTA file (batch mode)")
27
+
28
+ parser.add_argument("--verbose", action="store_true", help="Verbose output")
29
+
30
+ args = parser.parse_args()
31
+
32
+ try:
33
+ from factorforge.engines.v1_archived.evaluation.metrics import BiologicalMetrics
34
+ from factorforge.engines.v1_archived.tokenization.codon_tokenizer import CodonTokenizer
35
+ except ImportError as exc:
36
+ raise SystemExit(
37
+ "v1 dependencies not installed. Install with: pip install -e \".[v1]\""
38
+ ) from exc
39
+
40
+ # Load tokenizer
41
+ if args.verbose:
42
+ print(f"Loading tokenizer from {args.model}...")
43
+
44
+ tokenizer_path = Path(args.model) / "tokenizer"
45
+ if not tokenizer_path.exists():
46
+ print(f"Error: Tokenizer not found at {tokenizer_path}")
47
+ return
48
+
49
+ tokenizer = CodonTokenizer.load(str(tokenizer_path))
50
+
51
+ # Get sequence
52
+ sequences = []
53
+ if Path(args.input).exists():
54
+ # File input
55
+ with open(args.input) as f:
56
+ content = f.read()
57
+
58
+ # Handle FASTA
59
+ if content.startswith(">"):
60
+ if args.batch:
61
+ sequences = parse_fasta(content)
62
+ else:
63
+ sequences = [extract_first_sequence(content)]
64
+ else:
65
+ sequences = [content.strip()]
66
+ else:
67
+ # Direct sequence input
68
+ sequences = [args.input]
69
+
70
+ # Process sequences
71
+ results = []
72
+ bio = BiologicalMetrics()
73
+
74
+ for i, seq in enumerate(sequences):
75
+ # Clean sequence
76
+ seq = "".join(c for c in seq.upper() if c in "ATGC")
77
+
78
+ if len(seq) == 0:
79
+ continue
80
+
81
+ if len(seq) % 3 != 0:
82
+ if args.verbose:
83
+ print(f"Warning: Sequence {i+1} length not multiple of 3, skipping...")
84
+ continue
85
+
86
+ # Tokenize
87
+ tokens = tokenizer.encode(seq)
88
+
89
+ # Metrics
90
+ metrics = bio.evaluate_sequence_quality(seq)
91
+ is_quality, checks = bio.is_high_quality(seq)
92
+
93
+ result = {
94
+ "sequence_id": i + 1,
95
+ "length": len(seq),
96
+ "num_tokens": len(tokens),
97
+ "compression_ratio": round(len(seq) / len(tokens), 2) if len(tokens) > 0 else 0,
98
+ "gc_content": metrics["gc_content"],
99
+ "cai": metrics["cai"],
100
+ "rare_codon_freq": metrics["rare_codon_freq"],
101
+ "high_quality": is_quality,
102
+ "quality_checks": checks,
103
+ }
104
+
105
+ results.append(result)
106
+
107
+ if args.verbose:
108
+ print(f"\nSequence {i+1}:")
109
+ print(f" Length: {result['length']} bp")
110
+ print(f" Tokens: {result['num_tokens']}")
111
+ print(f" GC%: {result['gc_content']:.1f}%")
112
+ print(f" CAI: {result['cai']:.3f}")
113
+ print(f" Quality: {'✅ PASS' if is_quality else '❌ FAIL'}")
114
+
115
+ # Output
116
+ if args.output:
117
+ with open(args.output, "w") as f:
118
+ json.dump(results, f, indent=2)
119
+ print(f"\n✅ Results saved to {args.output}")
120
+ else:
121
+ print("\n" + json.dumps(results, indent=2))
122
+
123
+
124
+ def parse_fasta(content):
125
+ """Parse FASTA file with multiple sequences"""
126
+ sequences = []
127
+ current_seq = []
128
+
129
+ for line in content.split("\n"):
130
+ if line.startswith(">"):
131
+ if current_seq:
132
+ sequences.append("".join(current_seq))
133
+ current_seq = []
134
+ else:
135
+ current_seq.append(line.strip())
136
+
137
+ if current_seq:
138
+ sequences.append("".join(current_seq))
139
+
140
+ return sequences
141
+
142
+
143
+ def extract_first_sequence(content):
144
+ """Extract first sequence from FASTA"""
145
+ lines = content.split("\n")
146
+ sequence = []
147
+
148
+ for line in lines:
149
+ if line.startswith(">"):
150
+ continue
151
+ sequence.append(line.strip())
152
+
153
+ return "".join(sequence)
154
+
155
+
156
+ if __name__ == "__main__":
157
+ main()
@@ -0,0 +1,305 @@
1
+ """
2
+ FactorForge CLI
3
+
4
+ Usage:
5
+ factorforge optimize input.fasta -e v2 -p balanced -o output.fasta
6
+ factorforge optimize input.fasta -e v2 -p balanced --template standard_expression -o output.gb --format genbank
7
+ factorforge list-engines
8
+ """
9
+
10
+ from pathlib import Path
11
+ import sys
12
+
13
+ import click
14
+
15
+ from factorforge import __version__
16
+ from factorforge.engines.registry import EngineRegistry
17
+ from factorforge.engines.v2.utils import parse_fasta_records
18
+
19
+
20
+ def _configure_stdio() -> None:
21
+ """Best-effort UTF-8 for Windows consoles."""
22
+ for stream in (sys.stdout, sys.stderr):
23
+ try:
24
+ reconfigure = getattr(stream, "reconfigure", None)
25
+ if callable(reconfigure):
26
+ reconfigure(encoding="utf-8")
27
+ except Exception:
28
+ pass
29
+
30
+
31
+ def _parse_csv_option(value):
32
+ """Parse comma-separated option values."""
33
+ if not value:
34
+ return None
35
+ parsed = [item.strip() for item in value.split(",") if item.strip()]
36
+ return parsed or None
37
+
38
+
39
+ def _wrap_sequence(sequence, width=80):
40
+ """Wrap sequence to fixed-width lines."""
41
+ return "\n".join(sequence[i : i + width] for i in range(0, len(sequence), width))
42
+
43
+
44
+ def _build_dp_result(sequence: str, objective: str, gc_min: float, gc_max: float):
45
+ """Run the constraint-based DP feasibility engine for a single protein sequence."""
46
+ if objective != "feasibility_best":
47
+ raise ValueError("DP engine currently supports --objective feasibility_best.")
48
+ if gc_min > gc_max:
49
+ raise ValueError("--gc-min must be <= --gc-max.")
50
+
51
+ from factorforge.engines.v3.metrics import load_codon_usage_table
52
+ from factorforge.ml.feasibility import analyze_feasibility
53
+
54
+ table = load_codon_usage_table()
55
+ result = analyze_feasibility(
56
+ sequence,
57
+ table.codon_weights,
58
+ target_gc_low=gc_min,
59
+ target_gc_high=gc_max,
60
+ )
61
+ best = result["target"]["best_candidate"]
62
+ feasible = best is not None
63
+ if best is None:
64
+ best = result["best_candidate_without_gc"]
65
+ if best is None:
66
+ raise ValueError("No DP candidate generated.")
67
+
68
+ reason = (
69
+ f"Maximum CAI under GC {gc_min:g}-{gc_max:g}%"
70
+ if feasible
71
+ else "Maximum CAI without GC constraint; requested GC range was infeasible"
72
+ )
73
+ return best, result, reason
74
+
75
+
76
+ def _format_dp_fasta(sequence_id: str, dna_sequence: str, cai: float, gc: float) -> str:
77
+ """Format a DP result as FASTA."""
78
+ header = f">{sequence_id}|engine=dp|objective=feasibility_best|cai={cai:.3f}|gc={gc:.2f}"
79
+ return f"{header}\n{_wrap_sequence(dna_sequence)}\n"
80
+
81
+
82
+ @click.group()
83
+ @click.version_option(version=__version__)
84
+ def cli():
85
+ """FactorForge - Codon Optimization Platform"""
86
+ _configure_stdio()
87
+
88
+
89
+ @cli.command()
90
+ def list_engines():
91
+ """List available optimization engines"""
92
+ engines = EngineRegistry.list_engines()
93
+
94
+ click.echo("\nAvailable Engines:\n")
95
+ for name, info in engines.items():
96
+ click.echo(f" - {name}: {info['name']} v{info['version']}")
97
+ click.echo()
98
+
99
+
100
+ @cli.command()
101
+ @click.argument("input_file", type=click.Path(exists=True))
102
+ @click.option(
103
+ "--engine",
104
+ "-e",
105
+ default="dp",
106
+ type=click.Choice(["dp", "v2"], case_sensitive=False),
107
+ help="Engine (dp, v2)",
108
+ )
109
+ @click.option("--profile", "-p", default="balanced", help="Optimization profile")
110
+ @click.option(
111
+ "--objective",
112
+ default="feasibility_best",
113
+ type=click.Choice(["feasibility_best", "gc_target", "high_cai"], case_sensitive=False),
114
+ help="DP objective",
115
+ )
116
+ @click.option("--gc-min", type=float, default=40.0, help="Minimum target GC percentage")
117
+ @click.option("--gc-max", type=float, default=55.0, help="Maximum target GC percentage")
118
+ @click.option("--template", "construct_template", help="Construct template name")
119
+ @click.option("--output", "-o", help="Output file")
120
+ @click.option("--format", "output_format", default="fasta", help="Output format (fasta, genbank)")
121
+ @click.option(
122
+ "--scan-mode",
123
+ default="full",
124
+ type=click.Choice(["full", "fast"], case_sensitive=False),
125
+ help="Rule scan mode",
126
+ )
127
+ @click.option("--scan-include", help="Comma-separated scanner names to include")
128
+ @click.option("--scan-exclude", help="Comma-separated scanner names to exclude")
129
+ def optimize(
130
+ input_file,
131
+ engine,
132
+ profile,
133
+ objective,
134
+ gc_min,
135
+ gc_max,
136
+ construct_template,
137
+ output,
138
+ output_format,
139
+ scan_mode,
140
+ scan_include,
141
+ scan_exclude,
142
+ ):
143
+ """Optimize protein sequence"""
144
+ try:
145
+ # Read file
146
+ with open(input_file, encoding="utf-8") as f:
147
+ raw_input = f.read()
148
+
149
+ scan_include_list = _parse_csv_option(scan_include)
150
+ scan_exclude_list = _parse_csv_option(scan_exclude)
151
+
152
+ fasta_records = None
153
+ sequence = raw_input.strip()
154
+ if raw_input.lstrip().startswith(">"):
155
+ fasta_records = parse_fasta_records(raw_input)
156
+ if len(fasta_records) == 1:
157
+ sequence = fasta_records[0][1]
158
+
159
+ if fasta_records is not None and len(fasta_records) > 1:
160
+ if engine == "dp":
161
+ raise ValueError("Multi-FASTA input requires --engine v2.")
162
+ if construct_template:
163
+ raise ValueError("Multi-FASTA input does not support --template mode.")
164
+ if output_format.lower() != "fasta":
165
+ raise ValueError("Multi-FASTA input only supports FASTA output.")
166
+
167
+ optimizer = EngineRegistry.get(engine)
168
+ payload = [{"id": seq_id, "sequence": seq} for seq_id, seq in fasta_records]
169
+ if hasattr(optimizer, "optimize_batch"):
170
+ results = optimizer.optimize_batch(
171
+ payload,
172
+ profile=profile,
173
+ scan_mode=scan_mode,
174
+ scan_include=scan_include_list,
175
+ scan_exclude=scan_exclude_list,
176
+ )
177
+ else:
178
+ results = [
179
+ optimizer.optimize(
180
+ seq,
181
+ profile=profile,
182
+ scan_mode=scan_mode,
183
+ scan_include=scan_include_list,
184
+ scan_exclude=scan_exclude_list,
185
+ )
186
+ for _id, seq in fasta_records
187
+ ]
188
+
189
+ combined_fasta = []
190
+ for idx, result in enumerate(results):
191
+ seq_id = payload[idx]["id"]
192
+ cai = result.metrics.get("cai", 0.0)
193
+ gc = result.metrics.get("gc_percent", result.metrics.get("gc_content", 0.0))
194
+ score = result.metrics.get("score", 0.0)
195
+ header = (
196
+ f">{seq_id}|profile={profile}|cai={float(cai):.3f}|"
197
+ f"gc={float(gc):.2f}|score={float(score):.3f}"
198
+ )
199
+ combined_fasta.append(f"{header}\n{_wrap_sequence(result.sequence)}")
200
+ out_content = "\n".join(combined_fasta) + "\n"
201
+
202
+ if output:
203
+ with open(output, "w", encoding="utf-8") as f:
204
+ f.write(out_content)
205
+ click.echo(f"Saved batch FASTA to: {output}")
206
+ else:
207
+ click.echo(f"\n{out_content}")
208
+ click.echo(f"Batch optimized: {len(results)} sequences")
209
+ return
210
+
211
+ if engine == "dp":
212
+ if construct_template:
213
+ raise ValueError("DP engine does not support --template mode.")
214
+ if output_format.lower() != "fasta":
215
+ raise ValueError("DP engine only supports FASTA output.")
216
+
217
+ best, feasibility, recommendation_reason = _build_dp_result(
218
+ sequence,
219
+ objective=objective,
220
+ gc_min=gc_min,
221
+ gc_max=gc_max,
222
+ )
223
+ dna_sequence = best["dna_sequence"]
224
+ cai = float(best["cai"])
225
+ gc = float(best["gc"])
226
+ sequence_id = Path(input_file).stem or "factorforge_dp"
227
+ fasta = _format_dp_fasta(sequence_id, dna_sequence, cai, gc)
228
+
229
+ click.echo("Optimizing with DP feasibility engine...")
230
+ if output:
231
+ with open(output, "w", encoding="utf-8") as f:
232
+ f.write(fasta)
233
+ click.echo(f"Saved to: {output}")
234
+ else:
235
+ click.echo(f"\n{fasta}")
236
+
237
+ click.echo("Metrics:")
238
+ click.echo(f" - cai: {cai:.3f}")
239
+ click.echo(f" - gc_percent: {gc:.2f}")
240
+ click.echo(f" - target_gc_min: {float(feasibility['target']['gc_low']):.2f}")
241
+ click.echo(f" - target_gc_max: {float(feasibility['target']['gc_high']):.2f}")
242
+ click.echo(f" - target_feasible: {bool(feasibility['target']['best_candidate'])}")
243
+ click.echo(f" - recommendation_reason: {recommendation_reason}")
244
+ return
245
+
246
+ if engine == "v2" and construct_template:
247
+ from factorforge.engines.v2.pipeline import OptimizationPipeline
248
+
249
+ pipeline = OptimizationPipeline(profile=profile, construct_template=construct_template)
250
+ result = pipeline.run(
251
+ sequence,
252
+ scan_mode=scan_mode,
253
+ scan_include=scan_include_list,
254
+ scan_exclude=scan_exclude_list,
255
+ )
256
+
257
+ if output_format.lower() == "genbank" and not output:
258
+ raise ValueError("GenBank output requires --output file path.")
259
+
260
+ if output:
261
+ result.save(Path(output), format=output_format)
262
+ click.echo(f"Saved to: {output}")
263
+ else:
264
+ click.echo(f"\n{result.sequence}\n")
265
+
266
+ click.echo("Metrics:")
267
+ for key, value in result.metadata.get("metrics", {}).items():
268
+ click.echo(f" - {key}: {value}")
269
+ else:
270
+ if output_format.lower() != "fasta":
271
+ raise ValueError("Non-FASTA output requires --template with v2 pipeline.")
272
+
273
+ # Get engine
274
+ optimizer = EngineRegistry.get(engine)
275
+
276
+ # Optimize
277
+ click.echo(f"Optimizing with {optimizer.name} v{optimizer.version}...")
278
+ result = optimizer.optimize(
279
+ sequence,
280
+ profile=profile,
281
+ scan_mode=scan_mode,
282
+ scan_include=scan_include_list,
283
+ scan_exclude=scan_exclude_list,
284
+ )
285
+
286
+ # Output results
287
+ if output:
288
+ with open(output, "w", encoding="utf-8") as f:
289
+ f.write(result.sequence)
290
+ click.echo(f"Saved to: {output}")
291
+ else:
292
+ click.echo(f"\n{result.sequence}\n")
293
+
294
+ # Output metrics
295
+ click.echo("Metrics:")
296
+ for key, value in result.metrics.items():
297
+ click.echo(f" - {key}: {value}")
298
+
299
+ except Exception as e:
300
+ click.echo(f"Error: {e}", err=True)
301
+ raise click.Abort()
302
+
303
+
304
+ if __name__ == "__main__":
305
+ cli()
@@ -0,0 +1,7 @@
1
+ """Core interfaces for extensibility"""
2
+
3
+ from .exporter import Exporter
4
+ from .optimizer import OptimizationResult, OptimizerEngine
5
+ from .validator import Validator
6
+
7
+ __all__ = ["OptimizerEngine", "OptimizationResult", "Validator", "Exporter"]
@@ -0,0 +1,13 @@
1
+ """Exporter interface"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+ from typing import Any
7
+
8
+
9
+ class Exporter(ABC):
10
+ @abstractmethod
11
+ def export(self, data: Any, format: str) -> str:
12
+ """Export data to the requested format."""
13
+ raise NotImplementedError
@@ -0,0 +1,85 @@
1
+ """
2
+ Optimizer Engine Interface
3
+
4
+ Interface that all optimization engines (v1, v2, v3...) must implement
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from abc import ABC, abstractmethod
10
+ from typing import Any
11
+
12
+
13
+ class OptimizationResult:
14
+ """Optimization result"""
15
+
16
+ def __init__(
17
+ self,
18
+ sequence: str,
19
+ metrics: dict[str, float],
20
+ metadata: dict[str, Any] | None = None,
21
+ ) -> None:
22
+ self.sequence = sequence
23
+ self.metrics = metrics
24
+ self.metadata = metadata or {}
25
+
26
+
27
+ class OptimizerEngine(ABC):
28
+ """Abstract optimization engine interface"""
29
+
30
+ @property
31
+ @abstractmethod
32
+ def name(self) -> str:
33
+ """Engine name"""
34
+ ...
35
+
36
+ @property
37
+ @abstractmethod
38
+ def version(self) -> str:
39
+ """Engine version"""
40
+ ...
41
+
42
+ @abstractmethod
43
+ def optimize(
44
+ self,
45
+ sequence: str,
46
+ profile: str | None = None,
47
+ **kwargs: Any,
48
+ ) -> OptimizationResult:
49
+ """
50
+ Optimize a sequence
51
+
52
+ Args:
53
+ sequence: Input protein sequence
54
+ profile: Optimization profile (e.g., balanced, high_gc)
55
+ **kwargs: Additional parameters
56
+
57
+ Returns:
58
+ OptimizationResult
59
+ """
60
+ ...
61
+
62
+ @abstractmethod
63
+ def validate(self, sequence: str) -> bool:
64
+ """
65
+ Validate input
66
+
67
+ Args:
68
+ sequence: Sequence to validate
69
+
70
+ Returns:
71
+ bool: True if valid
72
+ """
73
+ ...
74
+
75
+ def get_metadata(self) -> dict[str, Any]:
76
+ """Engine metadata"""
77
+ return {
78
+ "name": self.name,
79
+ "version": self.version,
80
+ "supported_profiles": self.get_supported_profiles(),
81
+ }
82
+
83
+ def get_supported_profiles(self) -> list[str]:
84
+ """List of supported profiles"""
85
+ return []
@@ -0,0 +1,9 @@
1
+ """Validator interface"""
2
+
3
+ from abc import ABC, abstractmethod
4
+
5
+
6
+ class Validator(ABC):
7
+ @abstractmethod
8
+ def validate(self, data: str) -> bool:
9
+ pass