pyweb-stack 0.1.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 (54) hide show
  1. pyweb/__init__.py +71 -0
  2. pyweb/app.py +37 -0
  3. pyweb/app_loader.py +139 -0
  4. pyweb/asgi.py +71 -0
  5. pyweb/auth.py +701 -0
  6. pyweb/bench.py +101 -0
  7. pyweb/browser.py +232 -0
  8. pyweb/build.py +295 -0
  9. pyweb/cache.py +201 -0
  10. pyweb/cli/__init__.py +465 -0
  11. pyweb/cli/__main__.py +3 -0
  12. pyweb/compiler/__init__.py +5 -0
  13. pyweb/compiler/ast.py +45 -0
  14. pyweb/compiler/codegen/__init__.py +9 -0
  15. pyweb/compiler/codegen/ir.py +50 -0
  16. pyweb/compiler/errors.py +29 -0
  17. pyweb/compiler/lower.py +733 -0
  18. pyweb/compiler/parser.py +532 -0
  19. pyweb/compiler/pipeline.py +158 -0
  20. pyweb/compiler/pyjs.py +1087 -0
  21. pyweb/compiler/rpc.py +35 -0
  22. pyweb/context.py +193 -0
  23. pyweb/css.py +115 -0
  24. pyweb/db/__init__.py +652 -0
  25. pyweb/db/migrate.py +143 -0
  26. pyweb/decorators.py +49 -0
  27. pyweb/deploy.py +102 -0
  28. pyweb/forms.py +108 -0
  29. pyweb/hosting.py +139 -0
  30. pyweb/jobs.py +248 -0
  31. pyweb/live.py +88 -0
  32. pyweb/lsp.py +214 -0
  33. pyweb/models.py +269 -0
  34. pyweb/npm.py +270 -0
  35. pyweb/observability.py +313 -0
  36. pyweb/platform.py +69 -0
  37. pyweb/plugins.py +112 -0
  38. pyweb/py.typed +0 -0
  39. pyweb/realtime.py +189 -0
  40. pyweb/rpc.py +137 -0
  41. pyweb/runtime/browser/runtime.js +1151 -0
  42. pyweb/runtime/server/__init__.py +445 -0
  43. pyweb/security.py +162 -0
  44. pyweb/serve.py +335 -0
  45. pyweb/ssr.py +260 -0
  46. pyweb/sync.py +118 -0
  47. pyweb/testing.py +154 -0
  48. pyweb/uploads.py +94 -0
  49. pyweb_stack-0.1.0.dist-info/METADATA +198 -0
  50. pyweb_stack-0.1.0.dist-info/RECORD +54 -0
  51. pyweb_stack-0.1.0.dist-info/WHEEL +5 -0
  52. pyweb_stack-0.1.0.dist-info/entry_points.txt +2 -0
  53. pyweb_stack-0.1.0.dist-info/licenses/LICENSE +21 -0
  54. pyweb_stack-0.1.0.dist-info/top_level.txt +1 -0
pyweb/models.py ADDED
@@ -0,0 +1,269 @@
1
+ """Model layer: annotation-style prototype models plus typed Field models
2
+ bound to a database driver (SQLite/Postgres)."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import sqlite3
7
+ import threading
8
+ from typing import ClassVar
9
+
10
+ _lock = threading.RLock()
11
+ _db_path = ":memory:"
12
+ _conns: dict[str, sqlite3.Connection] = {}
13
+
14
+
15
+ class Email(str):
16
+ @classmethod
17
+ def validate(cls, value):
18
+ if "@" not in str(value):
19
+ raise ValueError(f"invalid email: {value!r}")
20
+ return cls(value)
21
+
22
+
23
+ class Field:
24
+ column_type = "TEXT"
25
+
26
+ def __init__(self, *, primary_key=False, nullable=True, default=None):
27
+ self.primary_key = primary_key
28
+ self.nullable = nullable
29
+ self.default = default
30
+ self.name = ""
31
+
32
+ def sql(self):
33
+ parts = [self.name, self.column_type]
34
+ if self.primary_key:
35
+ parts.append("PRIMARY KEY")
36
+ if self.column_type == "INTEGER":
37
+ parts.append("AUTOINCREMENT")
38
+ elif not self.nullable:
39
+ parts.append("NOT NULL")
40
+ if self.default is not None and not self.primary_key:
41
+ parts.append(f"DEFAULT {self._default_sql()}")
42
+ return " ".join(parts)
43
+
44
+ def _default_sql(self):
45
+ if isinstance(self.default, str):
46
+ return "'" + self.default.replace("'", "''") + "'"
47
+ if isinstance(self.default, bool):
48
+ return "1" if self.default else "0"
49
+ return str(self.default)
50
+
51
+ def to_db(self, value):
52
+ return value
53
+
54
+ def from_db(self, value):
55
+ return value
56
+
57
+
58
+ class IntegerField(Field):
59
+ column_type = "INTEGER"
60
+
61
+
62
+ class TextField(Field):
63
+ column_type = "TEXT"
64
+
65
+
66
+ class RealField(Field):
67
+ column_type = "REAL"
68
+
69
+
70
+ class BlobField(Field):
71
+ column_type = "BLOB"
72
+
73
+
74
+ class ModelMeta(type):
75
+ _registry: list[type] = []
76
+
77
+ def __new__(mcls, name, bases, ns):
78
+ fields: dict[str, Field] = {}
79
+ for base in bases:
80
+ if hasattr(base, "__fields__") and isinstance(getattr(base, "__fields__"), dict):
81
+ for k, f in getattr(base, "__fields__").items():
82
+ if isinstance(f, Field):
83
+ fields[k] = f
84
+ annotations = ns.get("__annotations__", {})
85
+ cls = super().__new__(mcls, name, bases, ns)
86
+ if name != "Model":
87
+ cls._fields = dict(annotations)
88
+ for key, val in list(ns.items()):
89
+ if isinstance(val, Field):
90
+ val.name = key
91
+ fields[key] = val
92
+ if fields:
93
+ cls.__fields__ = fields
94
+ if "__table__" not in ns:
95
+ cls.__table__ = name.lower()
96
+ pks = [k for k, f in fields.items() if f.primary_key]
97
+ cls.__pk__ = pks[0] if pks else None
98
+ elif "__fields__" not in ns:
99
+ cls.__fields__ = {}
100
+ ModelMeta._registry.append(cls)
101
+ return cls
102
+
103
+
104
+ class Model(metaclass=ModelMeta):
105
+ _fields: dict = {}
106
+ _table: str = ""
107
+ __fields__: ClassVar[dict] = {}
108
+ __table__: ClassVar[str] = ""
109
+ __pk__: ClassVar[str | None] = None
110
+ _db: ClassVar = None
111
+
112
+ def __init__(self, **kwargs):
113
+ if getattr(type(self), "__fields__", {}):
114
+ for key, field in type(self).__fields__.items():
115
+ setattr(self, key, kwargs.pop(key, field.default))
116
+ if kwargs:
117
+ raise TypeError(f"unknown fields for {type(self).__name__}: {sorted(kwargs)}")
118
+ else:
119
+ for k, v in kwargs.items():
120
+ setattr(self, k, v)
121
+
122
+ @classmethod
123
+ def bind(cls, db):
124
+ cls._db = db
125
+ return db
126
+
127
+ @classmethod
128
+ def _db_or_raise(cls):
129
+ if cls._db is None:
130
+ raise RuntimeError(f"{cls.__name__} is not bound to a database; "
131
+ f"call {cls.__name__}.bind(db) first")
132
+ return cls._db
133
+
134
+ def to_dict(self):
135
+ if getattr(type(self), "__fields__", {}):
136
+ return {k: getattr(self, k) for k in type(self).__fields__}
137
+ return dict(self.__dict__)
138
+
139
+ @classmethod
140
+ def schema_sql(cls):
141
+ cols = ", ".join(f.sql() for f in cls.__fields__.values())
142
+ return f"CREATE TABLE IF NOT EXISTS {cls.__table__} ({cols})"
143
+
144
+ @classmethod
145
+ def _from_row(cls, row, columns):
146
+ obj = cls.__new__(cls)
147
+ for col, val in zip(columns, row):
148
+ field = cls.__fields__.get(col)
149
+ setattr(obj, col, field.from_db(val) if field else val)
150
+ return obj
151
+
152
+ def save(self):
153
+ from pyweb.db import Query
154
+ db = self._db_or_raise()
155
+ data = {k: f.to_db(getattr(self, k)) for k, f in type(self).__fields__.items()
156
+ if not (f.primary_key and getattr(self, k) is None)}
157
+ pk = type(self).__pk__
158
+ if pk is not None and getattr(self, pk) is not None:
159
+ assignments = {k: v for k, v in data.items() if k != pk}
160
+ if assignments:
161
+ q = Query(type(self).__table__).where(**{pk: getattr(self, pk)})
162
+ sql, params = q.build_update(assignments)
163
+ db.execute(sql, params)
164
+ else:
165
+ sql, params = Query(type(self).__table__).build_insert(data)
166
+ result = db.execute(sql, params)
167
+ if pk is not None and getattr(self, pk) is None:
168
+ setattr(self, pk, result.lastrowid)
169
+ return self
170
+
171
+ def delete(self):
172
+ from pyweb.db import Query
173
+ db = self._db_or_raise()
174
+ pk = type(self).__pk__
175
+ if pk is None or getattr(self, pk) is None:
176
+ raise ValueError("cannot delete a model without a primary key value")
177
+ q = Query(type(self).__table__).where(**{pk: getattr(self, pk)})
178
+ sql, params = q.build_delete()
179
+ db.execute(sql, params)
180
+
181
+ @classmethod
182
+ def get(cls, pk_value):
183
+ from pyweb.db import Query
184
+ db = cls._db_or_raise()
185
+ q = Query(cls.__table__).where(**{cls.__pk__: pk_value})
186
+ sql, params = q.build_select()
187
+ res = db.execute(sql, params)
188
+ rows = res.fetchall()
189
+ if not rows:
190
+ return None
191
+ return cls._from_row(rows[0], res.columns)
192
+
193
+ @classmethod
194
+ def filter(cls, **kwargs):
195
+ from pyweb.db import Query
196
+ db = cls._db_or_raise()
197
+ q = Query(cls.__table__).where(**kwargs)
198
+ sql, params = q.build_select()
199
+ res = db.execute(sql, params)
200
+ return [cls._from_row(r, res.columns) for r in res.fetchall()]
201
+
202
+ @classmethod
203
+ def _conn(cls):
204
+ global _db_path
205
+ key = _db_path
206
+ with _lock:
207
+ if key not in _conns:
208
+ _conns[key] = sqlite3.connect(key, check_same_thread=False)
209
+ _conns[key].row_factory = sqlite3.Row
210
+ return _conns[key]
211
+
212
+ @classmethod
213
+ def configure(cls, path=":memory:"):
214
+ global _db_path
215
+ _db_path = path
216
+
217
+ @classmethod
218
+ def table(cls):
219
+ return cls._table or cls.__name__.lower() + "s"
220
+
221
+ @classmethod
222
+ def migrate(cls):
223
+ cols = []
224
+ for name, ann in cls._fields.items():
225
+ tname = getattr(ann, "__name__", "TEXT").upper()
226
+ sql_t = {"STR": "TEXT", "INT": "INTEGER", "FLOAT": "REAL", "BOOL": "INTEGER"}.get(tname, "TEXT")
227
+ cols.append(f'"{name}" {sql_t}')
228
+ conn = cls._conn()
229
+ with _lock:
230
+ conn.execute(f'CREATE TABLE IF NOT EXISTS "{cls.table()}" (id INTEGER PRIMARY KEY AUTOINCREMENT, {", ".join(cols)})')
231
+ conn.commit()
232
+
233
+ @classmethod
234
+ def create(cls, **kwargs):
235
+ if getattr(cls, "__fields__", {}):
236
+ return cls(**kwargs).save()
237
+ cls.migrate()
238
+ conn = cls._conn()
239
+ keys = list(kwargs)
240
+ with _lock:
241
+ cur = conn.execute(
242
+ f'INSERT INTO "{cls.table()}" ({", ".join(keys)}) VALUES ({", ".join("?" for _ in keys)})',
243
+ [kwargs[k] for k in keys],
244
+ )
245
+ conn.commit()
246
+ return {"id": cur.lastrowid, **kwargs}
247
+
248
+ @classmethod
249
+ def all(cls):
250
+ if getattr(cls, "__fields__", {}):
251
+ from pyweb.db import Query
252
+ db = cls._db_or_raise()
253
+ sql, params = Query(cls.__table__).build_select()
254
+ res = db.execute(sql, params)
255
+ return [cls._from_row(r, res.columns) for r in res.fetchall()]
256
+ cls.migrate()
257
+ conn = cls._conn()
258
+ with _lock:
259
+ return [dict(r) for r in conn.execute(f'SELECT * FROM "{cls.table()}"')]
260
+
261
+ @classmethod
262
+ def where(cls, **kwargs):
263
+ cls.migrate()
264
+ conn = cls._conn()
265
+ with _lock:
266
+ if not kwargs:
267
+ return cls.all()
268
+ cond = " AND ".join(f'"{k}" = ?' for k in kwargs)
269
+ return [dict(r) for r in conn.execute(f'SELECT * FROM "{cls.table()}" WHERE {cond}', list(kwargs.values()))]
pyweb/npm.py ADDED
@@ -0,0 +1,270 @@
1
+ """npm interop stub: typed package references + .d.ts binding sketch."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from pathlib import Path
7
+
8
+
9
+ def package(name: str, version: str = "latest"):
10
+ return {"npm": name, "version": version, "kind": "esm"}
11
+
12
+
13
+ _TS_SCALARS = {"number": "float", "string": "str", "boolean": "bool",
14
+ "any": "object", "unknown": "object", "void": "None",
15
+ "null": "None", "undefined": "None", "Date": "str"}
16
+
17
+
18
+ def _ts_to_py(ts_type):
19
+ import re
20
+ t = ts_type.strip().rstrip(";").strip()
21
+ if t.endswith("[]"):
22
+ return f"list[{_ts_to_py(t[:-2])}]"
23
+ m = re.match(r"Array<(.+)>", t)
24
+ if m:
25
+ return f"list[{_ts_to_py(m.group(1))}]"
26
+ if "|" in t:
27
+ parts = [_ts_to_py(p) for p in t.split("|")]
28
+ parts = [p for p in parts if p != "None"]
29
+ if len(parts) != len(t.split("|")):
30
+ return f"Optional[{' | '.join(parts) if parts else 'object'}]"
31
+ return " | ".join(parts) if parts else "object"
32
+ m = re.match(r"Record<(.+),\s*(.+)>", t)
33
+ if m:
34
+ return f"dict[{_ts_to_py(m.group(1))}, {_ts_to_py(m.group(2))}]"
35
+ return _TS_SCALARS.get(t, t)
36
+
37
+
38
+ def from_dts(dts: str):
39
+ """Parse `interface`/`type` declarations into typed Python dataclasses."""
40
+ import re
41
+ out = ["from __future__ import annotations", "from dataclasses import dataclass",
42
+ "from typing import Optional", ""]
43
+ found = False
44
+ for m in re.finditer(r"(?:interface|type)\s+(\w+)(?:\s*=\s*)?\s*\{([^}]*)\}", dts):
45
+ found = True
46
+ name, body = m.group(1), m.group(2)
47
+ out.append("@dataclass")
48
+ out.append(f"class {name}:")
49
+ n_fields = 0
50
+ for line in body.splitlines():
51
+ line = line.split("//")[0].strip().rstrip(",;")
52
+ fm = re.match(r"(?:readonly\s+)?(\w+)(\?)?:\s*(.+)", line)
53
+ if fm:
54
+ fname, opt, ftype = fm.group(1), fm.group(2), fm.group(3).strip()
55
+ py_t = _ts_to_py(ftype)
56
+ out.append(f" {fname}: {py_t} = None" if opt else f" {fname}: {py_t}")
57
+ n_fields += 1
58
+ if not n_fields:
59
+ out.append(" pass")
60
+ out.append("")
61
+ for m in re.finditer(r"type\s+(\w+)\s*=\s*([^;]+);", dts):
62
+ if m.group(1) not in dts:
63
+ out.append(f"{m.group(1)} = {_ts_to_py(m.group(2))}")
64
+ return "\n".join(out) if found else "\n".join(out)
65
+
66
+
67
+ @dataclass
68
+ class DtsField:
69
+ name: str
70
+ ts_type: str
71
+ py_type: str
72
+ optional: bool
73
+ nested: str | None = None
74
+
75
+
76
+ @dataclass
77
+ class DtsInterface:
78
+ name: str
79
+ base: str | None
80
+ fields: list = field(default_factory=list)
81
+
82
+
83
+ _INTERFACE_RE = None
84
+ _FIELD_RE = None
85
+
86
+
87
+ def _get_interface_re():
88
+ import re as _re
89
+ global _INTERFACE_RE
90
+ if _INTERFACE_RE is None:
91
+ _INTERFACE_RE = _re.compile(
92
+ r"interface\s+(?P<name>[A-Za-z_][\w]*)\s*"
93
+ r"(?:extends\s+(?P<base>[A-Za-z_][\w]*))?\s*\{(?P<body>[^}]*)\}",
94
+ _re.DOTALL)
95
+ return _INTERFACE_RE
96
+
97
+
98
+ def _get_field_re():
99
+ import re as _re
100
+ global _FIELD_RE
101
+ if _FIELD_RE is None:
102
+ _FIELD_RE = _re.compile(
103
+ r"(?P<name>[A-Za-z_][\w]*)(?P<opt>\?)?\s*:\s*(?P<type>[^;,\n]+)\s*;?,?\s*$")
104
+ return _FIELD_RE
105
+
106
+
107
+ def _ts_to_py_typed(ts_type, known):
108
+ import re as _re
109
+ _KNOWN_SCALARS = {"string": "str", "number": "float", "boolean": "bool",
110
+ "any": "Any", "unknown": "Any", "void": "None",
111
+ "null": "None", "undefined": "None", "object": "dict"}
112
+ t = ts_type.strip()
113
+ nested = None
114
+
115
+ def scalar(tok):
116
+ tok = tok.strip()
117
+ if (tok.startswith('"') and tok.endswith('"')) or (
118
+ tok.startswith("'") and tok.endswith("'")):
119
+ return "str"
120
+ if tok in _KNOWN_SCALARS:
121
+ return _KNOWN_SCALARS[tok]
122
+ if _re.fullmatch(r"-?\d+", tok):
123
+ return "int"
124
+ if _re.fullmatch(r"-?\d*\.\d+", tok):
125
+ return "float"
126
+ if tok in known:
127
+ return tok
128
+ return "Any"
129
+
130
+ m = _re.fullmatch(r"Array\s*<\s*(.+)\s*>", t)
131
+ if m:
132
+ inner, nested = _ts_to_py_typed(m.group(1), known)
133
+ return f"list[{inner}]", nested
134
+ while t.endswith("[]"):
135
+ t = t[:-2].strip()
136
+ inner, nested = _ts_to_py_typed(t, known)
137
+ return f"list[{inner}]", nested
138
+ if "|" in t:
139
+ parts = [p.strip() for p in t.split("|")]
140
+ parts = [p for p in parts if p not in ("undefined", "null")]
141
+ if parts and all((p.startswith('"') or p.startswith("'")) for p in parts):
142
+ return "str", None
143
+ if len(parts) == 1:
144
+ return _ts_to_py_typed(parts[0], known)
145
+ non_any = [scalar(p) for p in parts if scalar(p) != "Any"]
146
+ if len(non_any) == 1:
147
+ return non_any[0], (non_any[0] if non_any[0] in known else None)
148
+ return "Any", None
149
+ base = scalar(t)
150
+ if base in known:
151
+ nested = base
152
+ return base, nested
153
+
154
+
155
+ def parse_dts(source):
156
+ found = list(_get_interface_re().finditer(source))
157
+ known = {m.group("name") for m in found}
158
+ out = []
159
+ for m in found:
160
+ iface = DtsInterface(name=m.group("name"), base=m.group("base"))
161
+ for raw_line in m.group("body").splitlines():
162
+ line = raw_line.strip()
163
+ if not line or line.startswith("//"):
164
+ continue
165
+ fm = _get_field_re().match(line)
166
+ if not fm:
167
+ continue
168
+ py_type, nested = _ts_to_py_typed(fm.group("type"), known)
169
+ iface.fields.append(DtsField(
170
+ name=fm.group("name"), ts_type=fm.group("type").strip(),
171
+ py_type=py_type, optional=bool(fm.group("opt")), nested=nested))
172
+ out.append(iface)
173
+ return out
174
+
175
+
176
+ def _coerce_typed(nested, py_type, value_expr):
177
+ if nested is None:
178
+ return value_expr
179
+ if py_type.startswith("list["):
180
+ return (f"[{nested}.from_dict(v) if isinstance(v, dict) else v "
181
+ f"for v in {value_expr}]")
182
+ return (f"{nested}.from_dict({value_expr}) "
183
+ f"if isinstance({value_expr}, dict) else {value_expr}")
184
+
185
+
186
+ def generate_stub(interfaces):
187
+ lines = [
188
+ '"""Generated stubs - do not edit. Source: .d.ts via pyweb npm.',
189
+ "",
190
+ "Regenerate with: pyweb npm <file.d.ts> [-o out.py]",
191
+ '"""',
192
+ "from __future__ import annotations",
193
+ "",
194
+ "from dataclasses import dataclass",
195
+ "from typing import Any, Optional",
196
+ "",
197
+ "",
198
+ ]
199
+ known = {i.name for i in interfaces}
200
+ for base in dict.fromkeys(i.base for i in interfaces if i.base and i.base not in known):
201
+ lines += ["@dataclass", f"class {base}:", " pass", "", ""]
202
+ for iface in interfaces:
203
+ base = f"({iface.base})" if iface.base else ""
204
+ lines.append("@dataclass")
205
+ lines.append(f"class {iface.name}{base}:")
206
+ fields = sorted(iface.fields, key=lambda f: f.optional)
207
+ if not fields and not iface.base:
208
+ lines.append(" pass")
209
+ else:
210
+ for f in fields:
211
+ ann = f"Optional[{f.py_type}]" if f.optional else f.py_type
212
+ default = " = None" if f.optional else ""
213
+ lines.append(f" {f.name}: {ann}{default}")
214
+ lines.append("")
215
+ lines.append(" @classmethod")
216
+ lines.append(f" def from_dict(cls, data: dict) -> {iface.name}:")
217
+ lines.append(" if not isinstance(data, dict):")
218
+ lines.append(f' raise TypeError(f"{iface.name} must be a dict")')
219
+ lines.append(" kwargs: dict[str, Any] = {}")
220
+ for f in fields:
221
+ if f.optional:
222
+ val = _coerce_typed(f.nested, f.py_type, f"data[{f.name!r}]")
223
+ lines.append(f" if data.get({f.name!r}) is not None:")
224
+ lines.append(f" kwargs[{f.name!r}] = {val}")
225
+ else:
226
+ val = _coerce_typed(f.nested, f.py_type, "_v")
227
+ lines.append(f" if {f.name!r} not in data:")
228
+ lines.append(f' raise TypeError(f"{iface.name} missing required field: {f.name}")')
229
+ lines.append(f" _v = data[{f.name!r}]")
230
+ lines.append(f" kwargs[{f.name!r}] = {val}")
231
+ lines.append(" try:")
232
+ lines.append(" return cls(**kwargs)")
233
+ lines.append(" except TypeError as exc:")
234
+ lines.append(f' raise TypeError(f"invalid {iface.name}: {{exc}}") from exc')
235
+ lines.append("")
236
+ lines.append("")
237
+ lines.append(f"def validate_{iface.name}(data: dict) -> {iface.name}:")
238
+ lines.append(f' """Validate a dict against {iface.name}; raise TypeError if invalid."""')
239
+ lines.append(f" return {iface.name}.from_dict(data)")
240
+ lines.append("")
241
+ lines.append("")
242
+ return "\n".join(lines).rstrip("\n") + "\n"
243
+
244
+
245
+ def generate_stubs_from_file(dts_path, out_path=None):
246
+ source = Path(dts_path).read_text(encoding="utf-8")
247
+ code = generate_stub(parse_dts(source))
248
+ if out_path is not None:
249
+ Path(out_path).write_text(code, encoding="utf-8")
250
+ return code
251
+
252
+
253
+ def npm_main(argv=None):
254
+ import argparse
255
+ ap = argparse.ArgumentParser(prog="pyweb npm",
256
+ description="Generate typed Python stubs from .d.ts")
257
+ ap.add_argument("dts", help="Input .d.ts file")
258
+ ap.add_argument("-o", "--out", default=None,
259
+ help="Write stubs to file (default: stdout)")
260
+ args = ap.parse_args(argv)
261
+ try:
262
+ code = generate_stubs_from_file(args.dts, args.out)
263
+ except FileNotFoundError:
264
+ print(f"npm: {args.dts}: file not found")
265
+ return 1
266
+ if args.out:
267
+ print(f"npm: wrote {args.out}")
268
+ else:
269
+ print(code, end="")
270
+ return 0