rdfcodegen 0.1.0__tar.gz

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,169 @@
1
+ Metadata-Version: 2.3
2
+ Name: rdfcodegen
3
+ Version: 0.1.0
4
+ Summary: Add your description here
5
+ Author: carlosmpv
6
+ Author-email: carlosmpv <carlosmpvaldes@gmail.com>
7
+ Requires-Python: >=3.14
8
+ Description-Content-Type: text/markdown
9
+
10
+ # rdfcodegen
11
+
12
+ Generate typed code interfaces from RDF/Turtle ontologies (e.g., schema.org).
13
+
14
+ **Currently supports TypeScript output. Additional language generators planned.**
15
+
16
+ ## In this README :point_down:
17
+
18
+ - [Features](#features)
19
+ - [Installation](#installation)
20
+ - [Usage](#usage)
21
+ - [Command line](#command-line)
22
+ - [Python API](#python-api)
23
+ - [Filtering specific classes](#filtering-specific-classes)
24
+ - [How it works](#how-it-works)
25
+ - [Roadmap](#roadmap)
26
+ - [Projects using this template](#projects-using-this-template)
27
+ - [FAQ](#faq)
28
+ - [Contributing](#contributing)
29
+
30
+ ## Features
31
+
32
+ **rdfcodegen** parses RDF/Turtle ontology files (like [schema.org](https://schema.org/)) and generates language-specific code with full type definitions, documentation, and inheritance.
33
+
34
+ :rocket: **What it produces:**
35
+ - Type-safe interfaces/classes extracted from RDFS/OWL class hierarchies.
36
+ - JSDoc/Docstring comments pulled from `rdfs:comment` annotations.
37
+ - Automatic dependency resolution — request one class and get all its parent types and field types transitively.
38
+ - Primitive type mapping (e.g., `schema:Text` → `string`, `schema:Number` → `number`).
39
+
40
+ :computer: **Supported output languages:**
41
+ - [x] **TypeScript** — `interface` definitions with optional properties (`?`) and array types.
42
+ - [ ] Go (planned)
43
+ - [ ] Python (planned)
44
+
45
+ ## Installation
46
+
47
+ ```bash
48
+ pip install rdfcodegen
49
+ ```
50
+
51
+ *Requires Python 3.9+ and [rdflib](https://github.com/RDFLib/rdflib).*
52
+
53
+ ## Usage
54
+
55
+ ### Command line
56
+
57
+ Generate TypeScript interfaces for all schema.org types:
58
+
59
+ ```bash
60
+ rdfcodegen --ttl https://schema.org/version/latest/schemaorg-current-https.ttl \
61
+ --namespace https://schema.org/ \
62
+ --out schema.ts
63
+ ```
64
+
65
+ Generate only specific classes (and everything they depend on):
66
+
67
+ ```bash
68
+ rdfcodegen --class schema:Person --class schema:Organization --out schema.ts
69
+ ```
70
+
71
+ Write to stdout (`-`):
72
+
73
+ ```bash
74
+ rdfcodegen > schema.ts
75
+ ```
76
+
77
+ **CLI options:**
78
+
79
+ | Option | Default | Description |
80
+ |---|---|---|
81
+ | `--ttl` | `schema.org/version/latest/schemaorg-current-https.ttl` | URL or path to Turtle file |
82
+ | `--namespace` | `https://schema.org/` | Ontology namespace URI |
83
+ | `--out` | `-` | Output file (`-` for stdout) |
84
+ | `--class` | (all classes) | Generate specific classes (repeatable) |
85
+
86
+ ### Python API
87
+
88
+ ```python
89
+ from rdfcodegen import TypescriptGenerator
90
+
91
+ gen = TypescriptGenerator()
92
+
93
+ # Generate all types
94
+ ts_code = gen.generate(
95
+ ttl_url="https://schema.org/version/latest/schemaorg-current-https.ttl",
96
+ namespace_url="https://schema.org/",
97
+ )
98
+
99
+ # Generate a subset
100
+ ts_code = gen.generate(
101
+ chosen=["schema:Person", "schema:Event"],
102
+ ttl_url="https://schema.org/version/latest/schemaorg-current-https.ttl",
103
+ namespace_url="https://schema.org/",
104
+ )
105
+
106
+ with open("schema.ts", "w") as f:
107
+ f.write(ts_code)
108
+ ```
109
+
110
+ ### Filtering specific classes
111
+
112
+ When you specify classes with `--class` (or the `chosen` parameter), the generator:
113
+
114
+ 1. Includes the requested classes.
115
+ 2. Recursively includes all **parent types** up the `rdfs:subClassOf` hierarchy.
116
+ 3. Recursively includes all **field types** referenced via `schema:rangeIncludes`.
117
+ 4. Skips primitive types (e.g., `schema:Text`, `schema:Number`) — these map directly to language primitives.
118
+
119
+ This ensures the generated output is always self-contained and compilable.
120
+
121
+ ## How it works
122
+
123
+ 1. **Parse** — The Turtle file is loaded into an RDF graph using `rdflib`.
124
+ 2. **Discover classes** — All subjects typed as `rdfs:Class` within the target namespace are collected.
125
+ 3. **Traverse hierarchy** — `rdfs:subClassOf` relationships are followed to build the class hierarchy.
126
+ 4. **Collect properties** — `schema:domainIncludes` and `schema:rangeIncludes` are used to determine which fields belong to which classes and what types they return.
127
+ 5. **Extract documentation** — `rdfs:comment` values become JSDoc/docstring comments.
128
+ 6. **Resolve dependencies** — If filtering is requested, a dependency graph walk collects all transitive type references.
129
+ 7. **Generate output** — A language-specific generator (e.g., `TypescriptGenerator`) emits the final code.
130
+
131
+ ### TypeScript output example
132
+
133
+ From `schema:Person`, the generator produces:
134
+
135
+ ```typescript
136
+ // THIS FILE IS AUTOMATICALLY GENERATED. DO NOT EDIT DIRECTLY.
137
+
138
+ /**
139
+ * A person (alive, dead, undead, or fictional).
140
+ */
141
+ export interface SchemaPerson extends SchemaThing {
142
+ /**
143
+ * An additional name for a Person, can be used for a middle name.
144
+ */
145
+ additionalName?: string[];
146
+ /**
147
+ * Physical address of the item.
148
+ */
149
+ address?: (SchemaPostalAddress | string)[];
150
+ /**
151
+ * Date of birth.
152
+ */
153
+ birthDate?: string[];
154
+ // ... more fields
155
+ }
156
+ ```
157
+
158
+ ## Roadmap
159
+
160
+ - [ ] Typescript types generator
161
+ - [ ] Helper function to generate JSON-LD from those structs
162
+ - [ ] Go struct generator with JSON tags
163
+ - [ ] Python dataclass/pydantic generator
164
+ - [ ] Choose other languages
165
+
166
+ ## Contributing
167
+
168
+ If you find a bug :bug:, please open a [bug report](https://github.com/allenai/python-package-template/issues/new?assignees=&labels=bug&template=bug_report.md&title=).
169
+ If you have an idea for an improvement or new feature :rocket:, please open a [feature request](https://github.com/allenai/python-package-template/issues/new?assignees=&labels=Feature+request&template=feature_request.md&title=).
@@ -0,0 +1,160 @@
1
+ # rdfcodegen
2
+
3
+ Generate typed code interfaces from RDF/Turtle ontologies (e.g., schema.org).
4
+
5
+ **Currently supports TypeScript output. Additional language generators planned.**
6
+
7
+ ## In this README :point_down:
8
+
9
+ - [Features](#features)
10
+ - [Installation](#installation)
11
+ - [Usage](#usage)
12
+ - [Command line](#command-line)
13
+ - [Python API](#python-api)
14
+ - [Filtering specific classes](#filtering-specific-classes)
15
+ - [How it works](#how-it-works)
16
+ - [Roadmap](#roadmap)
17
+ - [Projects using this template](#projects-using-this-template)
18
+ - [FAQ](#faq)
19
+ - [Contributing](#contributing)
20
+
21
+ ## Features
22
+
23
+ **rdfcodegen** parses RDF/Turtle ontology files (like [schema.org](https://schema.org/)) and generates language-specific code with full type definitions, documentation, and inheritance.
24
+
25
+ :rocket: **What it produces:**
26
+ - Type-safe interfaces/classes extracted from RDFS/OWL class hierarchies.
27
+ - JSDoc/Docstring comments pulled from `rdfs:comment` annotations.
28
+ - Automatic dependency resolution — request one class and get all its parent types and field types transitively.
29
+ - Primitive type mapping (e.g., `schema:Text` → `string`, `schema:Number` → `number`).
30
+
31
+ :computer: **Supported output languages:**
32
+ - [x] **TypeScript** — `interface` definitions with optional properties (`?`) and array types.
33
+ - [ ] Go (planned)
34
+ - [ ] Python (planned)
35
+
36
+ ## Installation
37
+
38
+ ```bash
39
+ pip install rdfcodegen
40
+ ```
41
+
42
+ *Requires Python 3.9+ and [rdflib](https://github.com/RDFLib/rdflib).*
43
+
44
+ ## Usage
45
+
46
+ ### Command line
47
+
48
+ Generate TypeScript interfaces for all schema.org types:
49
+
50
+ ```bash
51
+ rdfcodegen --ttl https://schema.org/version/latest/schemaorg-current-https.ttl \
52
+ --namespace https://schema.org/ \
53
+ --out schema.ts
54
+ ```
55
+
56
+ Generate only specific classes (and everything they depend on):
57
+
58
+ ```bash
59
+ rdfcodegen --class schema:Person --class schema:Organization --out schema.ts
60
+ ```
61
+
62
+ Write to stdout (`-`):
63
+
64
+ ```bash
65
+ rdfcodegen > schema.ts
66
+ ```
67
+
68
+ **CLI options:**
69
+
70
+ | Option | Default | Description |
71
+ |---|---|---|
72
+ | `--ttl` | `schema.org/version/latest/schemaorg-current-https.ttl` | URL or path to Turtle file |
73
+ | `--namespace` | `https://schema.org/` | Ontology namespace URI |
74
+ | `--out` | `-` | Output file (`-` for stdout) |
75
+ | `--class` | (all classes) | Generate specific classes (repeatable) |
76
+
77
+ ### Python API
78
+
79
+ ```python
80
+ from rdfcodegen import TypescriptGenerator
81
+
82
+ gen = TypescriptGenerator()
83
+
84
+ # Generate all types
85
+ ts_code = gen.generate(
86
+ ttl_url="https://schema.org/version/latest/schemaorg-current-https.ttl",
87
+ namespace_url="https://schema.org/",
88
+ )
89
+
90
+ # Generate a subset
91
+ ts_code = gen.generate(
92
+ chosen=["schema:Person", "schema:Event"],
93
+ ttl_url="https://schema.org/version/latest/schemaorg-current-https.ttl",
94
+ namespace_url="https://schema.org/",
95
+ )
96
+
97
+ with open("schema.ts", "w") as f:
98
+ f.write(ts_code)
99
+ ```
100
+
101
+ ### Filtering specific classes
102
+
103
+ When you specify classes with `--class` (or the `chosen` parameter), the generator:
104
+
105
+ 1. Includes the requested classes.
106
+ 2. Recursively includes all **parent types** up the `rdfs:subClassOf` hierarchy.
107
+ 3. Recursively includes all **field types** referenced via `schema:rangeIncludes`.
108
+ 4. Skips primitive types (e.g., `schema:Text`, `schema:Number`) — these map directly to language primitives.
109
+
110
+ This ensures the generated output is always self-contained and compilable.
111
+
112
+ ## How it works
113
+
114
+ 1. **Parse** — The Turtle file is loaded into an RDF graph using `rdflib`.
115
+ 2. **Discover classes** — All subjects typed as `rdfs:Class` within the target namespace are collected.
116
+ 3. **Traverse hierarchy** — `rdfs:subClassOf` relationships are followed to build the class hierarchy.
117
+ 4. **Collect properties** — `schema:domainIncludes` and `schema:rangeIncludes` are used to determine which fields belong to which classes and what types they return.
118
+ 5. **Extract documentation** — `rdfs:comment` values become JSDoc/docstring comments.
119
+ 6. **Resolve dependencies** — If filtering is requested, a dependency graph walk collects all transitive type references.
120
+ 7. **Generate output** — A language-specific generator (e.g., `TypescriptGenerator`) emits the final code.
121
+
122
+ ### TypeScript output example
123
+
124
+ From `schema:Person`, the generator produces:
125
+
126
+ ```typescript
127
+ // THIS FILE IS AUTOMATICALLY GENERATED. DO NOT EDIT DIRECTLY.
128
+
129
+ /**
130
+ * A person (alive, dead, undead, or fictional).
131
+ */
132
+ export interface SchemaPerson extends SchemaThing {
133
+ /**
134
+ * An additional name for a Person, can be used for a middle name.
135
+ */
136
+ additionalName?: string[];
137
+ /**
138
+ * Physical address of the item.
139
+ */
140
+ address?: (SchemaPostalAddress | string)[];
141
+ /**
142
+ * Date of birth.
143
+ */
144
+ birthDate?: string[];
145
+ // ... more fields
146
+ }
147
+ ```
148
+
149
+ ## Roadmap
150
+
151
+ - [ ] Typescript types generator
152
+ - [ ] Helper function to generate JSON-LD from those structs
153
+ - [ ] Go struct generator with JSON tags
154
+ - [ ] Python dataclass/pydantic generator
155
+ - [ ] Choose other languages
156
+
157
+ ## Contributing
158
+
159
+ If you find a bug :bug:, please open a [bug report](https://github.com/allenai/python-package-template/issues/new?assignees=&labels=bug&template=bug_report.md&title=).
160
+ If you have an idea for an improvement or new feature :rocket:, please open a [feature request](https://github.com/allenai/python-package-template/issues/new?assignees=&labels=Feature+request&template=feature_request.md&title=).
@@ -0,0 +1,17 @@
1
+ [project]
2
+ name = "rdfcodegen"
3
+ version = "0.1.0"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "carlosmpv", email = "carlosmpvaldes@gmail.com" }
8
+ ]
9
+ requires-python = ">=3.14"
10
+ dependencies = []
11
+
12
+ [project.scripts]
13
+ rdfcodegen = "rdfcodegen:main"
14
+
15
+ [build-system]
16
+ requires = ["uv_build>=0.11.28,<0.12.0"]
17
+ build-backend = "uv_build"
@@ -0,0 +1,269 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from collections import defaultdict
5
+ from dataclasses import dataclass
6
+ import argparse
7
+ import sys
8
+
9
+ import rdflib
10
+ from rdflib import RDF, RDFS, URIRef
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class FieldDef:
15
+ field: str
16
+ comment: tuple[str, ...]
17
+ types: tuple[str, ...]
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class ClassDef:
22
+ comment: tuple[str, ...]
23
+ parents: tuple[str, ...]
24
+ fields: tuple[FieldDef, ...]
25
+
26
+
27
+ class RDFCodeGenerator(ABC):
28
+ def _qname_or_iri(self, g: rdflib.Graph, term: URIRef) -> str:
29
+ try:
30
+ return g.namespace_manager.normalizeUri(term)
31
+ except Exception:
32
+ return str(term)
33
+
34
+ def clean_comment(self, text: str) -> str:
35
+ return " ".join(text.split()).replace("*/", "* /")
36
+
37
+ def parse_ttl(
38
+ self,
39
+ ttl_url: str,
40
+ namespace_url: str,
41
+ ) -> dict[str, ClassDef]:
42
+ schema = rdflib.Namespace(namespace_url)
43
+ g = rdflib.Graph()
44
+ g.parse(ttl_url, format="turtle")
45
+
46
+ classes: set[URIRef] = set()
47
+ props_by_class: dict[URIRef, set[URIRef]] = defaultdict(set)
48
+ range_by_prop: dict[URIRef, set[URIRef]] = defaultdict(set)
49
+ class_comments: dict[URIRef, list[str]] = defaultdict(list)
50
+ prop_comments: dict[URIRef, list[str]] = defaultdict(list)
51
+ parents_by_class: dict[URIRef, set[URIRef]] = defaultdict(set)
52
+
53
+ for s in g.subjects(RDF.type, RDFS.Class):
54
+ if isinstance(s, URIRef) and str(s).startswith(str(schema)):
55
+ classes.add(s)
56
+
57
+ # fecha a hierarquia antes de gerar saída
58
+ queue = list(classes)
59
+ while queue:
60
+ cls = queue.pop()
61
+ for parent in g.objects(cls, RDFS.subClassOf):
62
+ if isinstance(parent, URIRef) and str(parent).startswith(str(schema)):
63
+ if parent not in classes:
64
+ classes.add(parent)
65
+ queue.append(parent)
66
+ parents_by_class[cls].add(parent)
67
+
68
+ for cls in classes:
69
+ for c in g.objects(cls, RDFS.comment):
70
+ if isinstance(c, rdflib.Literal):
71
+ class_comments[cls].append(str(c))
72
+
73
+ for prop, cls in g.subject_objects(schema.domainIncludes):
74
+ if isinstance(prop, URIRef) and isinstance(cls, URIRef):
75
+ if str(prop).startswith(str(schema)) and str(cls).startswith(str(schema)):
76
+ props_by_class[cls].add(prop)
77
+
78
+ for prop, rng in g.subject_objects(schema.rangeIncludes):
79
+ if isinstance(prop, URIRef) and isinstance(rng, URIRef):
80
+ if str(prop).startswith(str(schema)) and str(rng).startswith(str(schema)):
81
+ range_by_prop[prop].add(rng)
82
+
83
+ all_props = {p for props in props_by_class.values() for p in props}
84
+ for prop in all_props:
85
+ for c in g.objects(prop, RDFS.comment):
86
+ if isinstance(c, rdflib.Literal):
87
+ prop_comments[prop].append(str(c))
88
+
89
+ out: dict[str, ClassDef] = {}
90
+ for cls in sorted(classes, key=str):
91
+ class_key = self._qname_or_iri(g, cls)
92
+ fields: list[FieldDef] = []
93
+
94
+ for prop in sorted(props_by_class.get(cls, set()), key=str):
95
+ fields.append(
96
+ FieldDef(
97
+ field=self._qname_or_iri(g, prop),
98
+ comment=tuple(dict.fromkeys(prop_comments.get(prop, []))),
99
+ types=tuple(sorted({
100
+ self._qname_or_iri(g, t) for t in range_by_prop.get(prop, set())
101
+ })),
102
+ )
103
+ )
104
+
105
+ out[class_key] = ClassDef(
106
+ comment=tuple(dict.fromkeys(class_comments.get(cls, []))),
107
+ parents=tuple(
108
+ self._qname_or_iri(g, p)
109
+ for p in sorted(parents_by_class.get(cls, set()), key=str)
110
+ ),
111
+ fields=tuple(fields),
112
+ )
113
+
114
+ return out
115
+
116
+ def generate(self, chosen: list[str] | None = None, **kwargs) -> str:
117
+ chosen = chosen or []
118
+ out = self.parse_ttl(**kwargs)
119
+
120
+ if not chosen:
121
+ return self._generate(out)
122
+
123
+ filtered: dict[str, ClassDef] = {}
124
+ visited: set[str] = set()
125
+
126
+ def collect(key: str) -> None:
127
+ if key in visited or key not in out:
128
+ return
129
+ visited.add(key)
130
+ filtered[key] = out[key]
131
+
132
+ deps = set(out[key].parents)
133
+ for field in out[key].fields:
134
+ deps.update(field.types)
135
+
136
+ for dep in deps:
137
+ collect(dep)
138
+
139
+ for item in chosen:
140
+ collect(item)
141
+
142
+ return self._generate(filtered)
143
+
144
+ @abstractmethod
145
+ def _generate(self, defs: dict[str, ClassDef]) -> str:
146
+ ...
147
+
148
+
149
+ class TypescriptGenerator(RDFCodeGenerator):
150
+ primitive_ts_types = {
151
+ "schema:Boolean": "boolean",
152
+ "schema:Date": "string",
153
+ "schema:DateTime": "string",
154
+ "schema:Number": "number",
155
+ "schema:Float": "number",
156
+ "schema:Integer": "number",
157
+ "schema:Quantity": "number",
158
+ "schema:Distance": "number",
159
+ "schema:Duration": "number",
160
+ "schema:Energy": "number",
161
+ "schema:Mass": "number",
162
+ "schema:Text": "string",
163
+ "schema:CssSelectorType": "string",
164
+ "schema:PronounceableText": "string",
165
+ "schema:URL": "string",
166
+ "schema:XPathType": "string",
167
+ "schema:Time": "string",
168
+ }
169
+
170
+ def schema_to_ts_types(self, org: dict[str, ClassDef]) -> dict[str, ClassDef]:
171
+ def as_ts_class(name: str) -> str:
172
+ name = name.removeprefix("schema:")
173
+ return f"Schema{name}"
174
+
175
+ def as_ts_field(name: str) -> str:
176
+ return name.removeprefix("schema:")
177
+
178
+ result: dict[str, ClassDef] = {}
179
+
180
+ for class_name, data in org.items():
181
+ if class_name in self.primitive_ts_types:
182
+ continue
183
+
184
+ parents = tuple(
185
+ sorted({
186
+ self.primitive_ts_types[p] if p in self.primitive_ts_types else as_ts_class(p)
187
+ for p in data.parents
188
+ if p not in self.primitive_ts_types
189
+ })
190
+ )
191
+
192
+ fields: list[FieldDef] = []
193
+ for item in data.fields:
194
+ types = tuple(
195
+ sorted({
196
+ self.primitive_ts_types[t] if t in self.primitive_ts_types else as_ts_class(t)
197
+ for t in item.types
198
+ })
199
+ )
200
+ fields.append(
201
+ FieldDef(
202
+ field=as_ts_field(item.field),
203
+ comment=item.comment,
204
+ types=types,
205
+ )
206
+ )
207
+
208
+ result[as_ts_class(class_name)] = ClassDef(
209
+ comment=data.comment,
210
+ parents=parents,
211
+ fields=tuple(fields),
212
+ )
213
+
214
+ return result
215
+
216
+ def jsdoc(self, lines: tuple[str, ...], indent: str = "") -> list[str]:
217
+ lines = tuple([self.clean_comment(line) for line in lines if line.strip()])
218
+ if not lines:
219
+ return []
220
+ return [f"{indent}/**", *[f"{indent} * {line}" for line in lines], f"{indent} */"]
221
+
222
+ def ts_type(self, types: tuple[str, ...]) -> str:
223
+ if not types:
224
+ return "unknown[]"
225
+ if len(types) == 1:
226
+ return f"{types[0]}[]"
227
+ return f"({ ' | '.join(types) })[]"
228
+
229
+ def _generate(self, defs: dict[str, ClassDef]) -> str:
230
+ defs = self.schema_to_ts_types(defs)
231
+
232
+ result_lines = ["// THIS FILE IS AUTOMATICALLY GENERATED. DO NOT EDIT DIRECTLY."]
233
+
234
+ for class_name in sorted(defs):
235
+ data = defs[class_name]
236
+ result_lines += self.jsdoc(data.comment)
237
+
238
+ extends_part = f" extends {', '.join(data.parents)}" if data.parents else ""
239
+ result_lines.append(f"export interface {class_name}{extends_part} {{")
240
+
241
+ for field in data.fields:
242
+ result_lines += self.jsdoc(field.comment, indent=" ")
243
+ result_lines.append(f" {field.field}?: {self.ts_type(field.types)};")
244
+
245
+ result_lines.append("}\n")
246
+
247
+ return "\n".join(result_lines).rstrip() + "\n"
248
+
249
+
250
+ def main() -> None:
251
+ parser = argparse.ArgumentParser()
252
+ parser.add_argument("--ttl", default="https://schema.org/version/latest/schemaorg-current-https.ttl")
253
+ parser.add_argument("--namespace", default="https://schema.org/")
254
+ parser.add_argument("--out", default="-")
255
+ parser.add_argument("--class", dest="classes", action="append", default=[])
256
+ args = parser.parse_args()
257
+
258
+ gen = TypescriptGenerator()
259
+ result = gen.generate(args.classes, ttl_url=args.ttl, namespace_url=args.namespace)
260
+
261
+ if args.out == "-":
262
+ sys.stdout.write(result)
263
+ else:
264
+ with open(args.out, "w", encoding="utf-8") as f:
265
+ f.write(result)
266
+
267
+
268
+ if __name__ == "__main__":
269
+ main()