prismaa 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.
prismaa/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
prismaa/cli.py ADDED
@@ -0,0 +1,14 @@
1
+ import click
2
+
3
+
4
+ @click.group()
5
+ def main() -> None:
6
+ """Prismaa — Python Prisma client generator."""
7
+
8
+
9
+ @main.command()
10
+ @click.option("--schema", default="schema.prisma", show_default=True, help="Path to schema.prisma")
11
+ @click.option("--output", default=None, help="Output directory (overrides schema generator.output)")
12
+ def generate(schema: str, output: str | None) -> None:
13
+ """Generate the Python client from a Prisma schema."""
14
+ raise NotImplementedError("Generator not yet implemented — see PLAN.md Phase 3")
File without changes
File without changes
@@ -0,0 +1,31 @@
1
+ from .ast import (
2
+ Attribute,
3
+ AttributeArg,
4
+ AttributeValue,
5
+ Datasource,
6
+ Field,
7
+ FunctionCall,
8
+ Generator,
9
+ Model,
10
+ Schema,
11
+ )
12
+ from .lexer import LexError, Token, TokenKind, tokenize
13
+ from .parser import ParseError, parse
14
+
15
+ __all__ = [
16
+ "parse",
17
+ "tokenize",
18
+ "Schema",
19
+ "Datasource",
20
+ "Generator",
21
+ "Model",
22
+ "Field",
23
+ "Attribute",
24
+ "AttributeArg",
25
+ "AttributeValue",
26
+ "FunctionCall",
27
+ "Token",
28
+ "TokenKind",
29
+ "LexError",
30
+ "ParseError",
31
+ ]
prismaa/parser/ast.py ADDED
@@ -0,0 +1,146 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+
5
+
6
+ @dataclass
7
+ class FunctionCall:
8
+ """Represents a zero-argument function call in an attribute, e.g. autoincrement(), now()."""
9
+
10
+ name: str
11
+
12
+ def __repr__(self) -> str:
13
+ return f"{self.name}()"
14
+
15
+
16
+ # Valid types for attribute argument values.
17
+ AttributeValue = str | int | float | bool | list[str] | FunctionCall
18
+
19
+
20
+ @dataclass
21
+ class AttributeArg:
22
+ """A single argument inside an attribute's parentheses."""
23
+
24
+ name: str | None # None for positional arguments
25
+ value: AttributeValue
26
+
27
+
28
+ @dataclass
29
+ class Attribute:
30
+ """A field-level (@) or block-level (@@) attribute."""
31
+
32
+ name: str # without leading @ or @@
33
+ args: list[AttributeArg] = field(default_factory=list)
34
+
35
+ def arg(self, name: str) -> AttributeValue | None:
36
+ """Return the value of a named argument, or None if absent."""
37
+ for a in self.args:
38
+ if a.name == name:
39
+ return a.value
40
+ return None
41
+
42
+ def first_positional(self) -> AttributeValue | None:
43
+ """Return the first positional argument value, or None."""
44
+ for a in self.args:
45
+ if a.name is None:
46
+ return a.value
47
+ return None
48
+
49
+
50
+ @dataclass
51
+ class Field:
52
+ name: str
53
+ type: str # e.g. "String", "Int", "Company", "Unsupported"
54
+ is_list: bool
55
+ is_optional: bool
56
+ # For Unsupported("native_type") fields, holds the native type string.
57
+ native_type: str | None = None
58
+ attributes: list[Attribute] = field(default_factory=list)
59
+ doc_comment: str | None = None
60
+
61
+ @property
62
+ def is_unsupported(self) -> bool:
63
+ return self.type == "Unsupported"
64
+
65
+ @property
66
+ def is_relation(self) -> bool:
67
+ return any(a.name == "relation" for a in self.attributes)
68
+
69
+ @property
70
+ def column_name(self) -> str:
71
+ """Actual DB column name, respecting @map."""
72
+ for attr in self.attributes:
73
+ if attr.name == "map":
74
+ v = attr.first_positional()
75
+ if isinstance(v, str):
76
+ return v
77
+ return self.name
78
+
79
+ def has_attr(self, name: str) -> bool:
80
+ return any(a.name == name for a in self.attributes)
81
+
82
+ def get_attr(self, name: str) -> Attribute | None:
83
+ for a in self.attributes:
84
+ if a.name == name:
85
+ return a
86
+ return None
87
+
88
+
89
+ @dataclass
90
+ class Model:
91
+ name: str
92
+ fields: list[Field] = field(default_factory=list)
93
+ block_attributes: list[Attribute] = field(default_factory=list)
94
+ doc_comment: str | None = None
95
+
96
+ @property
97
+ def is_ignored(self) -> bool:
98
+ return any(a.name == "ignore" for a in self.block_attributes)
99
+
100
+ @property
101
+ def table_name(self) -> str:
102
+ """Actual DB table name, respecting @@map."""
103
+ for attr in self.block_attributes:
104
+ if attr.name == "map":
105
+ v = attr.first_positional()
106
+ if isinstance(v, str):
107
+ return v
108
+ return self.name
109
+
110
+ def scalar_fields(self) -> list[Field]:
111
+ """Non-relation, non-unsupported fields."""
112
+ return [f for f in self.fields if not f.is_relation and not f.is_unsupported]
113
+
114
+ def relation_fields(self) -> list[Field]:
115
+ return [f for f in self.fields if f.is_relation]
116
+
117
+
118
+ @dataclass
119
+ class Generator:
120
+ name: str
121
+ properties: dict[str, str] = field(default_factory=dict)
122
+
123
+ def get(self, key: str, default: str = "") -> str:
124
+ return self.properties.get(key, default)
125
+
126
+
127
+ @dataclass
128
+ class Datasource:
129
+ name: str
130
+ provider: str
131
+
132
+
133
+ @dataclass
134
+ class Schema:
135
+ datasource: Datasource | None = None
136
+ generators: list[Generator] = field(default_factory=list)
137
+ models: list[Model] = field(default_factory=list)
138
+
139
+ @property
140
+ def generator(self) -> Generator | None:
141
+ """The first generator block, or None."""
142
+ return self.generators[0] if self.generators else None
143
+
144
+ def active_models(self) -> list[Model]:
145
+ """Models that are not marked @@ignore."""
146
+ return [m for m in self.models if not m.is_ignored]
@@ -0,0 +1,103 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from dataclasses import dataclass
5
+ from enum import Enum, auto
6
+
7
+
8
+ class TokenKind(Enum):
9
+ IDENT = auto()
10
+ STRING = auto()
11
+ NUMBER = auto()
12
+ DOUBLE_AT = auto()
13
+ AT = auto()
14
+ LBRACE = auto()
15
+ RBRACE = auto()
16
+ LPAREN = auto()
17
+ RPAREN = auto()
18
+ LBRACKET = auto()
19
+ RBRACKET = auto()
20
+ COLON = auto()
21
+ COMMA = auto()
22
+ EQUALS = auto()
23
+ QUESTION = auto()
24
+ DOC_COMMENT = auto()
25
+ NEWLINE = auto()
26
+ EOF = auto()
27
+
28
+
29
+ @dataclass
30
+ class Token:
31
+ kind: TokenKind
32
+ value: str
33
+ line: int
34
+
35
+ def __repr__(self) -> str:
36
+ return f"Token({self.kind.name}, {self.value!r}, line={self.line})"
37
+
38
+
39
+ class LexError(Exception):
40
+ pass
41
+
42
+
43
+ # Rules are tested in order; first match wins.
44
+ # None means "skip this token" (whitespace, regular comments).
45
+ _RULES: list[tuple[TokenKind | None, str]] = [
46
+ (TokenKind.DOC_COMMENT, r"///[^\n]*"),
47
+ (None, r"//[^\n]*"), # regular comment — discard
48
+ (None, r"[ \t]+"), # horizontal whitespace — discard
49
+ (TokenKind.NEWLINE, r"\n"),
50
+ (TokenKind.STRING, r'"[^"\\]*(?:\\.[^"\\]*)*"'),
51
+ (TokenKind.NUMBER, r"\d+\.\d+|\d+"),
52
+ (TokenKind.DOUBLE_AT, r"@@"), # must precede AT
53
+ (TokenKind.AT, r"@"),
54
+ (TokenKind.LBRACE, r"\{"),
55
+ (TokenKind.RBRACE, r"\}"),
56
+ (TokenKind.LPAREN, r"\("),
57
+ (TokenKind.RPAREN, r"\)"),
58
+ (TokenKind.LBRACKET, r"\["),
59
+ (TokenKind.RBRACKET, r"\]"),
60
+ (TokenKind.COLON, r":"),
61
+ (TokenKind.COMMA, r","),
62
+ (TokenKind.EQUALS, r"="),
63
+ (TokenKind.QUESTION, r"\?"),
64
+ (TokenKind.IDENT, r"[A-Za-z_][A-Za-z0-9_]*"),
65
+ (None, r"\r"), # bare carriage return — discard
66
+ ]
67
+
68
+ _MASTER = re.compile(
69
+ "|".join(f"(?P<r{i}>{pattern})" for i, (_, pattern) in enumerate(_RULES)),
70
+ )
71
+
72
+
73
+ def tokenize(source: str) -> list[Token]:
74
+ """Lex *source* into a flat token list ending with an EOF token."""
75
+ tokens: list[Token] = []
76
+ line = 1
77
+
78
+ for m in _MASTER.finditer(source):
79
+ group_index = int(m.lastgroup[1:]) # "r7" → 7
80
+ kind, _ = _RULES[group_index]
81
+ value = m.group()
82
+
83
+ if value == "\n":
84
+ tokens.append(Token(TokenKind.NEWLINE, value, line))
85
+ line += 1
86
+ elif kind is not None:
87
+ tokens.append(Token(kind, value, line))
88
+ # kind is None → skip (whitespace / comments)
89
+
90
+ # Verify the entire source was consumed (no unexpected characters).
91
+ matched_len = sum(len(m.group()) for m in _MASTER.finditer(source))
92
+ if matched_len != len(source):
93
+ # Find the first unmatched character to give a useful error.
94
+ pos = 0
95
+ for m in _MASTER.finditer(source):
96
+ if m.start() != pos:
97
+ break
98
+ pos = m.end()
99
+ ctx = source[pos : pos + 20].replace("\n", "\\n")
100
+ raise LexError(f"Unexpected character at line {line}: {ctx!r}")
101
+
102
+ tokens.append(Token(TokenKind.EOF, "", line))
103
+ return tokens
@@ -0,0 +1,364 @@
1
+ from __future__ import annotations
2
+
3
+ from .ast import (
4
+ Attribute,
5
+ AttributeArg,
6
+ AttributeValue,
7
+ Datasource,
8
+ Field,
9
+ FunctionCall,
10
+ Generator,
11
+ Model,
12
+ Schema,
13
+ )
14
+ from .lexer import Token, TokenKind, tokenize
15
+
16
+
17
+ class ParseError(Exception):
18
+ pass
19
+
20
+
21
+ class Parser:
22
+ def __init__(self, tokens: list[Token]) -> None:
23
+ self._tokens = tokens
24
+ self._pos = 0
25
+
26
+ # ------------------------------------------------------------------
27
+ # Token stream primitives
28
+ # ------------------------------------------------------------------
29
+
30
+ def _peek(self) -> Token:
31
+ return self._tokens[self._pos]
32
+
33
+ def _advance(self) -> Token:
34
+ tok = self._tokens[self._pos]
35
+ if tok.kind != TokenKind.EOF:
36
+ self._pos += 1
37
+ return tok
38
+
39
+ def _at(self, kind: TokenKind) -> bool:
40
+ return self._peek().kind == kind
41
+
42
+ def _expect(self, kind: TokenKind) -> Token:
43
+ tok = self._peek()
44
+ if tok.kind != kind:
45
+ raise ParseError(
46
+ f"Expected {kind.name} but got {tok.kind.name} ({tok.value!r}) at line {tok.line}"
47
+ )
48
+ return self._advance()
49
+
50
+ def _skip_newlines(self) -> None:
51
+ while self._at(TokenKind.NEWLINE):
52
+ self._advance()
53
+
54
+ def _at_end_of_statement(self) -> bool:
55
+ return self._at(TokenKind.NEWLINE) or self._at(TokenKind.RBRACE) or self._at(TokenKind.EOF)
56
+
57
+ # ------------------------------------------------------------------
58
+ # Top-level
59
+ # ------------------------------------------------------------------
60
+
61
+ def parse(self) -> Schema:
62
+ schema = Schema()
63
+ pending_doc: str | None = None
64
+
65
+ while not self._at(TokenKind.EOF):
66
+ if self._at(TokenKind.NEWLINE):
67
+ self._advance()
68
+ continue
69
+
70
+ if self._at(TokenKind.DOC_COMMENT):
71
+ # Accumulate consecutive doc comment lines.
72
+ lines = []
73
+ while self._at(TokenKind.DOC_COMMENT):
74
+ lines.append(self._advance().value[3:].strip())
75
+ self._skip_newlines()
76
+ pending_doc = "\n".join(lines)
77
+ continue
78
+
79
+ if not self._at(TokenKind.IDENT):
80
+ self._advance()
81
+ continue
82
+
83
+ keyword = self._peek().value
84
+ if keyword == "generator":
85
+ schema.generators.append(self._parse_generator())
86
+ elif keyword == "datasource":
87
+ schema.datasource = self._parse_datasource()
88
+ elif keyword == "model":
89
+ model = self._parse_model()
90
+ model.doc_comment = pending_doc
91
+ schema.models.append(model)
92
+ else:
93
+ # Unknown top-level block (e.g. "enum", "type") — skip entirely.
94
+ self._skip_block()
95
+
96
+ pending_doc = None
97
+
98
+ return schema
99
+
100
+ # ------------------------------------------------------------------
101
+ # Blocks
102
+ # ------------------------------------------------------------------
103
+
104
+ def _parse_generator(self) -> Generator:
105
+ self._expect(TokenKind.IDENT) # consume "generator"
106
+ name = self._expect(TokenKind.IDENT).value
107
+ self._skip_newlines()
108
+ self._expect(TokenKind.LBRACE)
109
+ props: dict[str, str] = {}
110
+ while not self._at(TokenKind.RBRACE) and not self._at(TokenKind.EOF):
111
+ self._skip_newlines()
112
+ if self._at(TokenKind.RBRACE):
113
+ break
114
+ key = self._expect(TokenKind.IDENT).value
115
+ self._expect(TokenKind.EQUALS)
116
+ value = self._parse_scalar_value()
117
+ props[key] = str(value)
118
+ self._skip_newlines()
119
+ self._expect(TokenKind.RBRACE)
120
+ return Generator(name=name, properties=props)
121
+
122
+ def _parse_datasource(self) -> Datasource:
123
+ self._expect(TokenKind.IDENT) # consume "datasource"
124
+ name = self._expect(TokenKind.IDENT).value
125
+ self._skip_newlines()
126
+ self._expect(TokenKind.LBRACE)
127
+ provider = "unknown"
128
+ while not self._at(TokenKind.RBRACE) and not self._at(TokenKind.EOF):
129
+ self._skip_newlines()
130
+ if self._at(TokenKind.RBRACE):
131
+ break
132
+ key = self._expect(TokenKind.IDENT).value
133
+ self._expect(TokenKind.EQUALS)
134
+ value = self._parse_scalar_value()
135
+ if key == "provider":
136
+ provider = str(value)
137
+ self._skip_newlines()
138
+ self._expect(TokenKind.RBRACE)
139
+ return Datasource(name=name, provider=provider)
140
+
141
+ def _parse_model(self) -> Model:
142
+ self._expect(TokenKind.IDENT) # consume "model"
143
+ name = self._expect(TokenKind.IDENT).value
144
+ self._skip_newlines()
145
+ self._expect(TokenKind.LBRACE)
146
+ self._skip_newlines()
147
+
148
+ fields: list[Field] = []
149
+ block_attrs: list[Attribute] = []
150
+ pending_doc: str | None = None
151
+
152
+ while not self._at(TokenKind.RBRACE) and not self._at(TokenKind.EOF):
153
+ if self._at(TokenKind.NEWLINE):
154
+ self._advance()
155
+ continue
156
+
157
+ if self._at(TokenKind.DOC_COMMENT):
158
+ lines = []
159
+ while self._at(TokenKind.DOC_COMMENT):
160
+ lines.append(self._advance().value[3:].strip())
161
+ self._skip_newlines()
162
+ pending_doc = "\n".join(lines)
163
+ continue
164
+
165
+ if self._at(TokenKind.DOUBLE_AT):
166
+ block_attrs.append(self._parse_block_attribute())
167
+ pending_doc = None
168
+ continue
169
+
170
+ if self._at(TokenKind.IDENT):
171
+ f = self._parse_field()
172
+ f.doc_comment = pending_doc
173
+ fields.append(f)
174
+ pending_doc = None
175
+ continue
176
+
177
+ # Unexpected token inside model block — skip to next newline.
178
+ self._advance()
179
+
180
+ self._expect(TokenKind.RBRACE)
181
+ return Model(name=name, fields=fields, block_attributes=block_attrs)
182
+
183
+ def _skip_block(self) -> None:
184
+ """Skip an unknown top-level block (keyword name { ... })."""
185
+ self._advance() # keyword
186
+ if self._at(TokenKind.IDENT):
187
+ self._advance() # name
188
+ self._skip_newlines()
189
+ if not self._at(TokenKind.LBRACE):
190
+ return
191
+ self._advance() # {
192
+ depth = 1
193
+ while depth > 0 and not self._at(TokenKind.EOF):
194
+ tok = self._advance()
195
+ if tok.kind == TokenKind.LBRACE:
196
+ depth += 1
197
+ elif tok.kind == TokenKind.RBRACE:
198
+ depth -= 1
199
+
200
+ # ------------------------------------------------------------------
201
+ # Fields
202
+ # ------------------------------------------------------------------
203
+
204
+ def _parse_field(self) -> Field:
205
+ name = self._expect(TokenKind.IDENT).value
206
+ type_name, native_type, is_list, is_optional = self._parse_field_type()
207
+ attributes: list[Attribute] = []
208
+ while self._at(TokenKind.AT) and not self._at_end_of_statement():
209
+ attributes.append(self._parse_field_attribute())
210
+ # Consume the trailing newline (or leave RBRACE for the caller).
211
+ if self._at(TokenKind.NEWLINE):
212
+ self._advance()
213
+ return Field(
214
+ name=name,
215
+ type=type_name,
216
+ is_list=is_list,
217
+ is_optional=is_optional,
218
+ native_type=native_type,
219
+ attributes=attributes,
220
+ )
221
+
222
+ def _parse_field_type(self) -> tuple[str, str | None, bool, bool]:
223
+ """Return (type_name, native_type, is_list, is_optional)."""
224
+ type_name = self._expect(TokenKind.IDENT).value
225
+ native_type: str | None = None
226
+
227
+ # Handle Unsupported("native_type")
228
+ if type_name == "Unsupported" and self._at(TokenKind.LPAREN):
229
+ self._advance()
230
+ native_type = self._expect(TokenKind.STRING).value.strip('"')
231
+ self._expect(TokenKind.RPAREN)
232
+
233
+ is_list = False
234
+ is_optional = False
235
+
236
+ if self._at(TokenKind.LBRACKET):
237
+ self._advance()
238
+ self._expect(TokenKind.RBRACKET)
239
+ is_list = True
240
+ elif self._at(TokenKind.QUESTION):
241
+ self._advance()
242
+ is_optional = True
243
+
244
+ return type_name, native_type, is_list, is_optional
245
+
246
+ # ------------------------------------------------------------------
247
+ # Attributes
248
+ # ------------------------------------------------------------------
249
+
250
+ def _parse_field_attribute(self) -> Attribute:
251
+ """Parse a @name or @name(...) field attribute."""
252
+ self._expect(TokenKind.AT)
253
+ name = self._expect(TokenKind.IDENT).value
254
+ args = self._parse_attribute_args() if self._at(TokenKind.LPAREN) else []
255
+ return Attribute(name=name, args=args)
256
+
257
+ def _parse_block_attribute(self) -> Attribute:
258
+ """Parse a @@name or @@name(...) block attribute."""
259
+ self._expect(TokenKind.DOUBLE_AT)
260
+ name = self._expect(TokenKind.IDENT).value
261
+ args = self._parse_attribute_args() if self._at(TokenKind.LPAREN) else []
262
+ if self._at(TokenKind.NEWLINE):
263
+ self._advance()
264
+ return Attribute(name=name, args=args)
265
+
266
+ def _parse_attribute_args(self) -> list[AttributeArg]:
267
+ self._expect(TokenKind.LPAREN)
268
+ args: list[AttributeArg] = []
269
+ while not self._at(TokenKind.RPAREN) and not self._at(TokenKind.EOF):
270
+ args.append(self._parse_attribute_arg())
271
+ if self._at(TokenKind.COMMA):
272
+ self._advance()
273
+ self._expect(TokenKind.RPAREN)
274
+ return args
275
+
276
+ def _parse_attribute_arg(self) -> AttributeArg:
277
+ """Parse one argument: either `name: value` or a bare `value`."""
278
+ # Named argument: IDENT COLON value
279
+ if self._at(TokenKind.IDENT) and self._tokens[self._pos + 1].kind == TokenKind.COLON:
280
+ name = self._advance().value
281
+ self._advance() # consume ':'
282
+ value = self._parse_attr_value()
283
+ return AttributeArg(name=name, value=value)
284
+ # Positional argument
285
+ return AttributeArg(name=None, value=self._parse_attr_value())
286
+
287
+ def _parse_attr_value(self) -> AttributeValue:
288
+ tok = self._peek()
289
+
290
+ if tok.kind == TokenKind.STRING:
291
+ self._advance()
292
+ return tok.value[1:-1] # strip surrounding quotes
293
+
294
+ if tok.kind == TokenKind.NUMBER:
295
+ self._advance()
296
+ return float(tok.value) if "." in tok.value else int(tok.value)
297
+
298
+ if tok.kind == TokenKind.LBRACKET:
299
+ return self._parse_array()
300
+
301
+ if tok.kind == TokenKind.IDENT:
302
+ name = self._advance().value
303
+ if name == "true":
304
+ return True
305
+ if name == "false":
306
+ return False
307
+ # Function call: name()
308
+ if self._at(TokenKind.LPAREN):
309
+ self._advance()
310
+ self._expect(TokenKind.RPAREN)
311
+ return FunctionCall(name)
312
+ # Bare identifier (e.g. enum value, env("VAR") key)
313
+ return name
314
+
315
+ raise ParseError(f"Unexpected token in attribute value: {tok!r}")
316
+
317
+ def _parse_array(self) -> list[str]:
318
+ """Parse [ident, ident, ...] — used in @@unique, @relation fields/references."""
319
+ self._expect(TokenKind.LBRACKET)
320
+ items: list[str] = []
321
+ while not self._at(TokenKind.RBRACKET) and not self._at(TokenKind.EOF):
322
+ items.append(self._expect(TokenKind.IDENT).value)
323
+ if self._at(TokenKind.COMMA):
324
+ self._advance()
325
+ self._expect(TokenKind.RBRACKET)
326
+ return items
327
+
328
+ # ------------------------------------------------------------------
329
+ # Shared value parsing (generator / datasource properties)
330
+ # ------------------------------------------------------------------
331
+
332
+ def _parse_scalar_value(self) -> AttributeValue:
333
+ """Parse a simple key = value pair value (string, number, bool, or env(...))."""
334
+ tok = self._peek()
335
+ if tok.kind == TokenKind.STRING:
336
+ self._advance()
337
+ return tok.value[1:-1]
338
+ if tok.kind == TokenKind.NUMBER:
339
+ self._advance()
340
+ return float(tok.value) if "." in tok.value else int(tok.value)
341
+ if tok.kind == TokenKind.IDENT:
342
+ name = self._advance().value
343
+ if name == "true":
344
+ return True
345
+ if name == "false":
346
+ return False
347
+ # env("VAR") — return the raw identifier; resolution happens at runtime
348
+ if self._at(TokenKind.LPAREN):
349
+ self._advance()
350
+ inner = self._peek()
351
+ value = inner.value[1:-1] if inner.kind == TokenKind.STRING else inner.value
352
+ self._advance()
353
+ self._expect(TokenKind.RPAREN)
354
+ return value
355
+ return name
356
+ if tok.kind == TokenKind.LBRACKET:
357
+ return self._parse_array()
358
+ raise ParseError(f"Unexpected token in value: {tok!r}")
359
+
360
+
361
+ def parse(source: str) -> Schema:
362
+ """Parse a Prisma schema string and return a :class:`Schema` AST."""
363
+ tokens = tokenize(source)
364
+ return Parser(tokens).parse()
prismaa/py.typed ADDED
File without changes
File without changes
@@ -0,0 +1,51 @@
1
+ Metadata-Version: 2.4
2
+ Name: prismaa
3
+ Version: 0.1.0
4
+ Summary: A production-grade Python Prisma client
5
+ Project-URL: Homepage, https://github.com/laszlosragner/prismaa
6
+ Project-URL: Documentation, https://laszlosragner.github.io/prismaa
7
+ Project-URL: Repository, https://github.com/laszlosragner/prismaa
8
+ Project-URL: Issues, https://github.com/laszlosragner/prismaa/issues
9
+ Author-email: Laszlo Sragner <laszlo@hypergolic.co.uk>
10
+ License: MIT License
11
+
12
+ Copyright (c) 2026 xLaszlo
13
+
14
+ Permission is hereby granted, free of charge, to any person obtaining a copy
15
+ of this software and associated documentation files (the "Software"), to deal
16
+ in the Software without restriction, including without limitation the rights
17
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18
+ copies of the Software, and to permit persons to whom the Software is
19
+ furnished to do so, subject to the following conditions:
20
+
21
+ The above copyright notice and this permission notice shall be included in all
22
+ copies or substantial portions of the Software.
23
+
24
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
30
+ SOFTWARE.
31
+ License-File: LICENSE
32
+ Keywords: async,database,orm,prisma,sqlalchemy
33
+ Classifier: Development Status :: 3 - Alpha
34
+ Classifier: Framework :: AsyncIO
35
+ Classifier: Intended Audience :: Developers
36
+ Classifier: License :: OSI Approved :: MIT License
37
+ Classifier: Programming Language :: Python :: 3
38
+ Classifier: Programming Language :: Python :: 3.11
39
+ Classifier: Programming Language :: Python :: 3.12
40
+ Classifier: Programming Language :: Python :: 3.13
41
+ Classifier: Topic :: Database
42
+ Requires-Python: >=3.11
43
+ Requires-Dist: aiosqlite>=0.19
44
+ Requires-Dist: click>=8.0
45
+ Requires-Dist: jinja2>=3.1
46
+ Requires-Dist: pydantic>=2.0
47
+ Requires-Dist: sqlalchemy>=2.0
48
+ Description-Content-Type: text/markdown
49
+
50
+ # prismaa
51
+ Prisma python client with SQLAlchemy backend
@@ -0,0 +1,15 @@
1
+ prismaa/__init__.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
2
+ prismaa/cli.py,sha256=3gw4WZkrgzflwpjyHfJkqWf2xOZLZ2UITE8Mx9lqevE,524
3
+ prismaa/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ prismaa/engine/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ prismaa/generator/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ prismaa/parser/__init__.py,sha256=isNtMRhVmQHTfpGIAIP-brqM03g_v7N2kzexn5f2iuc,513
7
+ prismaa/parser/ast.py,sha256=VP5doDijft5xE_ixJdZIRjZUzfQcogdnptMz6BsqV1Y,4091
8
+ prismaa/parser/lexer.py,sha256=sWeXpQUO9PiaPnQjLc4XuDsBqMpNNL1CVCKKhOyY9Wk,2924
9
+ prismaa/parser/parser.py,sha256=-OSiI_qGmZtt-BN1X9ZiN-kCdKPn4dF9UVnNohLwnhg,13292
10
+ prismaa/types/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ prismaa-0.1.0.dist-info/METADATA,sha256=Kgzhb_gLNP8dYtLVafk0cXgQqVN8RuMBmjvPgfmxe5Q,2391
12
+ prismaa-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
13
+ prismaa-0.1.0.dist-info/entry_points.txt,sha256=wCauZPJ2MHIwy2WgtgEoF46cQ9-1--_HcQM82Q0HSH8,45
14
+ prismaa-0.1.0.dist-info/licenses/LICENSE,sha256=8_nxY_VIKgaz42SM-7ZACEIOnvNXVFth6YJG9n51_vs,1064
15
+ prismaa-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ prismaa = prismaa.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 xLaszlo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.