hypie 0.1.1.dev2__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.
@@ -0,0 +1,719 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Literal, get_args
5
+ import json
6
+ from functools import wraps
7
+
8
+ from textwrap import indent
9
+
10
+ import re
11
+
12
+
13
+ def python_to_hs(obj):
14
+ if isinstance(obj, Expr):
15
+ return obj.render()
16
+ if isinstance(obj, bool):
17
+ return "true" if obj else "false"
18
+ elif obj is None:
19
+ return "null"
20
+ elif isinstance(obj, (int, float)):
21
+ return f"{obj}"
22
+ elif isinstance(obj, str):
23
+ exp_template = VarTemplateString(obj)
24
+ if not exp_template.interpolations:
25
+ return repr(obj)
26
+ return exp_template.transform(
27
+ interp_func=lambda x: "${" + x + "}",
28
+ join_func=lambda parts: "`" + "".join(parts) + "`",
29
+ )
30
+ elif isinstance(obj, dict):
31
+ lines = []
32
+ for k, v in obj.items():
33
+ l = f"{k}: {python_to_hs(v)}"
34
+ lines.append(indent(l, " ", lambda _: True))
35
+ return "{\n" + ",\n".join(lines) + "\n}"
36
+ elif isinstance(obj, list):
37
+ lines = []
38
+ for v in obj:
39
+ l = python_to_hs(v)
40
+ lines.append(indent(l, " ", lambda _: True))
41
+ return "[\n" + ",\n".join(lines) + "\n]"
42
+ else:
43
+ return python_to_hs(str(obj))
44
+
45
+
46
+ def coerce_python_type_to_hs(v):
47
+ match v:
48
+ case bool():
49
+ return BooleanLiteral(v)
50
+ case int() | float():
51
+ return NumberLiteral(v)
52
+ case str():
53
+ return StringLiteral(v)
54
+ case list():
55
+ return ListLiteral(v)
56
+ case dict():
57
+ return ObjectLiteral(v)
58
+ case None:
59
+ return NullLiteral()
60
+ case _:
61
+ return v
62
+
63
+
64
+ def coerce_arguments(func):
65
+ @wraps(func)
66
+ def wrapped(*args, **kwargs):
67
+ new_args = []
68
+ for a in args:
69
+ new_args.append(coerce_python_type_to_hs(a))
70
+ new_kwargs = {}
71
+ for k, v in kwargs.items():
72
+ new_kwargs[k] = coerce_python_type_to_hs(v)
73
+ return func(*new_args, **new_kwargs)
74
+
75
+ return wrapped
76
+
77
+
78
+ def wrap_if_not_literal(v):
79
+ if isinstance(v, Expr) and not isinstance(v, get_args(Literals.__value__)):
80
+ return Parentheses(v)
81
+ return v
82
+
83
+
84
+ def wrap_expressions(func):
85
+ @wraps(func)
86
+ def wrapped(*args, **kwargs):
87
+ new_args = []
88
+ for a in args:
89
+ new_args.append(wrap_if_not_literal(a))
90
+ new_kwargs = {}
91
+ for k, v in kwargs.items():
92
+ new_kwargs[k] = wrap_if_not_literal(v)
93
+ return func(*new_args, **new_kwargs)
94
+
95
+ return wrapped
96
+
97
+
98
+ PATTERN = re.compile(r"\?VAR:<(.*?)>\?")
99
+
100
+
101
+ class VarTemplateString:
102
+ def __init__(self, source: str):
103
+ self.pattern = PATTERN
104
+ self.source = source
105
+ self.strings = []
106
+ self.interpolations = []
107
+ matches = re.finditer(self.pattern, self.source)
108
+ last_idx = 0
109
+ if matches:
110
+ for m in matches:
111
+ end_idx = m.span()[0]
112
+ s = self.source[last_idx:end_idx]
113
+ self.strings.append(s)
114
+ self.interpolations.append(m.group(1))
115
+ last_idx = m.span()[1]
116
+ # print(last_idx, len(source), print(source[last_idx]))
117
+ # if last_idx <= len(source) - 1:
118
+ self.strings.append(self.source[last_idx:])
119
+
120
+ else:
121
+ self.strings = source
122
+
123
+ def transform(
124
+ self,
125
+ string_func=lambda x: x,
126
+ interp_func=lambda x: x,
127
+ join_func=lambda parts: "".join(p for p in parts if p != ""),
128
+ ):
129
+ parts = [string_func(self.strings[0])]
130
+ for i, inter in enumerate(self.interpolations):
131
+ parts.append(interp_func(inter))
132
+ parts.append(string_func(self.strings[i + 1]))
133
+ return join_func(parts)
134
+
135
+ def __str__(self):
136
+ return f"VarTemplateString(strings={self.strings}, interpolations={self.interpolations})"
137
+
138
+ __repr__ = __str__
139
+
140
+
141
+ class Expr:
142
+ def __html__(self):
143
+ return "${" + self.render() + "}"
144
+
145
+ def __str__(self):
146
+ return f"?VAR:<{self.render()}>?"
147
+
148
+ # __html__ = __str__
149
+
150
+ def render(self):
151
+ pass
152
+
153
+ # def __format__(self, spec):
154
+ # return "${" + self.render() + "}"
155
+
156
+ # property access
157
+ def __getattr__(self, name: str):
158
+ # htpy attempts to check for this, which makes __html__ fail
159
+ if name == "iter_chunks":
160
+ raise AttributeError()
161
+ return PropertyAccess(self, property=name)
162
+
163
+ def prop(self, name: str):
164
+ return PropertyAccess(self, property=name)
165
+
166
+ def __getitem__(self, name: str | Expr):
167
+ return PropertyAccessBrackets(self, property=name)
168
+
169
+ # binary
170
+ @coerce_arguments
171
+ @wrap_expressions
172
+ def __add__(self, other: Expr):
173
+ return BinaryOp(kind="add", left=self, right=other)
174
+
175
+ @coerce_arguments
176
+ @wrap_expressions
177
+ def __sub__(self, other: Expr):
178
+ return BinaryOp(kind="subtract", left=self, right=other)
179
+
180
+ @coerce_arguments
181
+ @wrap_expressions
182
+ def __mul__(self, other: Expr):
183
+ return BinaryOp(kind="multiply", left=self, right=other)
184
+
185
+ @coerce_arguments
186
+ @wrap_expressions
187
+ def __mod__(self, other: Expr):
188
+ return BinaryOp(kind="mod", left=self, right=other)
189
+
190
+ @coerce_arguments
191
+ @wrap_expressions
192
+ def __gt__(self, other):
193
+ return BinaryOp(kind="gt", left=self, right=other)
194
+
195
+ @coerce_arguments
196
+ @wrap_expressions
197
+ def __ge__(self, other):
198
+ return BinaryOp(kind="gte", left=self, right=other)
199
+
200
+ @coerce_arguments
201
+ @wrap_expressions
202
+ def __lt__(self, other):
203
+ return BinaryOp(kind="lt", left=self, right=other)
204
+
205
+ @coerce_arguments
206
+ @wrap_expressions
207
+ def __le__(self, other):
208
+ return BinaryOp(kind="lte", left=self, right=other)
209
+
210
+ @coerce_arguments
211
+ @wrap_expressions
212
+ def __ne__(self, other):
213
+ return BinaryOp(kind="ne", left=self, right=other)
214
+
215
+ @coerce_arguments
216
+ @wrap_expressions
217
+ def and_(self, other: Expr):
218
+ return BinaryOp(kind="and", left=self, right=other)
219
+
220
+ @coerce_arguments
221
+ @wrap_expressions
222
+ def or_(self, other: Expr):
223
+ return BinaryOp(kind="or", left=self, right=other)
224
+
225
+ @coerce_arguments
226
+ @wrap_expressions
227
+ def matches(self, other: Expr):
228
+ return BinaryOp(kind="matches", left=self, right=other)
229
+
230
+ @coerce_arguments
231
+ @wrap_expressions
232
+ def does_not_match(self, other: Expr):
233
+ return BinaryOp(kind="does not match", left=self, right=other)
234
+
235
+ @coerce_arguments
236
+ @wrap_expressions
237
+ # collection
238
+ def where(self, condition: Expr):
239
+ return Where(collection=self, condition=condition)
240
+
241
+ @coerce_arguments
242
+ @wrap_expressions
243
+ def first(self):
244
+ return PositionalCollection(kind="first", collection=self)
245
+
246
+ @coerce_arguments
247
+ @wrap_expressions
248
+ def random(self):
249
+ return PositionalCollection(kind="random", collection=self)
250
+
251
+ @coerce_arguments
252
+ @wrap_expressions
253
+ def last(self):
254
+ return PositionalCollection(kind="last", collection=self)
255
+
256
+ @wrap_expressions
257
+ def __or__(self, type: Literal["JSON", "JSONString"]):
258
+ return TypeConversion(coerce_python_type_to_hs(self), type)
259
+
260
+ @wrap_expressions
261
+ def __ror__(type, self: Literal["JSON", "JSONString"]):
262
+ return TypeConversion(coerce_python_type_to_hs(self), type)
263
+
264
+ # @coerce_arguments
265
+ @wrap_expressions
266
+ def as_(self, type: Literal["JSON", "JSONString"]):
267
+ return TypeConversion(self, type)
268
+
269
+
270
+ ################## BASIC TYPES ##################
271
+
272
+
273
+ @dataclass
274
+ class StringLiteral(Expr):
275
+ value: str
276
+
277
+ def render(self):
278
+ return python_to_hs(self.value)
279
+
280
+
281
+ @dataclass
282
+ class TemplateLiteral(Expr):
283
+ value: str
284
+
285
+ def render(self):
286
+ return f"`{self.value}`"
287
+
288
+
289
+ @dataclass
290
+ class NumberLiteral(Expr):
291
+ value: int | float
292
+
293
+ def render(self):
294
+ return python_to_hs(self.value)
295
+
296
+
297
+ @dataclass
298
+ class BooleanLiteral(Expr):
299
+ value: Literal[True, False]
300
+
301
+ def render(self):
302
+ return python_to_hs(self.value)
303
+
304
+
305
+ @dataclass
306
+ class NullLiteral(Expr):
307
+ def render(self):
308
+ return python_to_hs(self.value)
309
+
310
+
311
+ @dataclass
312
+ class ListLiteral(Expr):
313
+ value: list
314
+
315
+ def render(self):
316
+ return python_to_hs(self.value)
317
+
318
+
319
+ @dataclass
320
+ class ObjectLiteral(Expr):
321
+ value: dict
322
+
323
+ def render(self):
324
+ return python_to_hs(self.value)
325
+
326
+
327
+ ################## DOM ##################
328
+ # class
329
+ @dataclass
330
+ class ClassDOMLiteral(Expr):
331
+ class_: str
332
+
333
+ def render(self):
334
+ exp_template = VarTemplateString(self.class_)
335
+ if not exp_template.interpolations:
336
+ return f".{self.class_}"
337
+ out = exp_template.transform(
338
+ string_func=lambda x: repr(x),
339
+ join_func=lambda parts: ".{"
340
+ + " + ".join(p for p in parts if p != repr(""))
341
+ + "}",
342
+ )
343
+ # print(exp_template)
344
+ # print(out)
345
+ return out
346
+
347
+ def in_(self, other: Expr):
348
+ return InDOM(self, other)
349
+
350
+
351
+ # query
352
+ @dataclass
353
+ class QueryDOMLiteral(Expr):
354
+ query: str
355
+
356
+ def render(self):
357
+ exp_template = VarTemplateString(self.query)
358
+ # print(exp_template)
359
+ if not exp_template.interpolations:
360
+ return f"<{self.query}/>"
361
+ out = exp_template.transform(
362
+ string_func=lambda x: x,
363
+ interp_func=lambda x: "${" + x + "}",
364
+ join_func=lambda parts: "<" + "".join(parts) + "/>",
365
+ )
366
+ # print(exp_template)
367
+ # print(out)
368
+ return out
369
+
370
+ def in_(self, other: Expr):
371
+ return InDOM(self, other)
372
+
373
+
374
+ # id
375
+ @dataclass
376
+ class IdDOMLiteral(Expr):
377
+ id_: str
378
+
379
+ def render(self):
380
+ exp_template = VarTemplateString(self.id_)
381
+ if not exp_template.interpolations:
382
+ return f"#{self.id_}"
383
+ out = exp_template.transform(
384
+ string_func=lambda x: repr(x),
385
+ join_func=lambda parts: "#{"
386
+ + " + ".join(p for p in parts if p != repr(""))
387
+ + "}",
388
+ )
389
+ # print(exp_template)
390
+ # print(out)
391
+ return out
392
+
393
+ def in_(self, other: Expr):
394
+ return InDOM(self, other)
395
+
396
+
397
+ @dataclass
398
+ class AttrLiteral(Expr):
399
+ attr_name: str
400
+ attr_value: str = None
401
+
402
+ def render(self):
403
+ rendered = f"@{self.attr_name}"
404
+ if self.attr_value:
405
+ rendered += f"='{json.dumps(self.attr_value)}'"
406
+ return rendered
407
+
408
+
409
+ type CSSExpr = IdDOMLiteral | QueryDOMLiteral | ClassDOMLiteral
410
+
411
+
412
+ ################## VARIABLE ##################
413
+ @dataclass
414
+ class VariableLiteral(Expr):
415
+ symbol: str
416
+
417
+ def render(self):
418
+ return f"{self.symbol}"
419
+
420
+
421
+ @dataclass
422
+ class TemplatedVariableLiteral(Expr):
423
+ symbol: str
424
+
425
+ def render(self):
426
+ return "${" + self.symbol + "}"
427
+
428
+ def __html__(self):
429
+ return self.render()
430
+
431
+ def __str__(self):
432
+ return self.render()
433
+
434
+
435
+ ################## Time ##################
436
+ type TIME_RES = Literal["s", "ms"]
437
+
438
+
439
+ @dataclass
440
+ class TimeLiteral(Expr):
441
+ time: int
442
+ resolution: TIME_RES = "ms"
443
+
444
+ def render(self):
445
+ return f"{self.time}{self.resolution}"
446
+
447
+
448
+ ################## Event ##################
449
+ @dataclass
450
+ class EventLiteral(Expr):
451
+ event_name: str
452
+ bind: dict[str, Expr] = field(default_factory=lambda: {})
453
+
454
+ def render(self):
455
+ rendered = f"{self.event_name}"
456
+ if self.bind:
457
+ rendered += "("
458
+ parts = []
459
+ for k, v in self.bind.items():
460
+ parts.append(f"{k}:{coerce_python_type_to_hs(v).render()}")
461
+ rendered += ", ".join(parts) + ")"
462
+ return rendered
463
+
464
+
465
+ @dataclass
466
+ class EventSpecLiteral(Expr):
467
+ event_name: str
468
+ args: list[str] = field(default_factory=lambda: [])
469
+ filter: Expr = None
470
+ count: int | tuple = None
471
+ from_: Expr | Literal["elsewhere"] = None
472
+ in_: Expr = None
473
+ debounce: TimeLiteral = None
474
+ throttle: TimeLiteral = None
475
+
476
+ def render(self):
477
+ rendered = f"{self.event_name}"
478
+ if self.args:
479
+ rendered += "("
480
+ parts = []
481
+ for a in self.args:
482
+ parts.append(a)
483
+ rendered += ", ".join(parts)
484
+ rendered += ")"
485
+ if self.filter:
486
+ rendered += f"[{self.filter.render()}]"
487
+ if self.count:
488
+ rendered += f" {self.count}"
489
+ if self.from_:
490
+ from_ = self.from_.render() if isinstance(self.from_, Expr) else self.from_
491
+ rendered += f" from {from_}"
492
+ if self.in_:
493
+ rendered += f" in {self.in_.render()}"
494
+ if self.debounce:
495
+ rendered += f" debounced at {self.debounce.render()}"
496
+ elif self.throttle:
497
+ rendered += f" throttled at {self.throttle.render()}"
498
+ return rendered
499
+
500
+
501
+ ################## DOM POSITONING ##################
502
+ @dataclass
503
+ class InDOM(Expr):
504
+ css_selector: CSSExpr
505
+ target: CSSExpr
506
+
507
+ def render(self):
508
+ return f"{self.css_selector.render()} in {self.target.render()}"
509
+
510
+
511
+ @dataclass
512
+ class RelativetDOM(Expr):
513
+ kind: Literal["next", "previous"]
514
+ css_selector: CSSExpr
515
+ from_: Expr | None = None
516
+ within: Expr | None = None
517
+ with_wrapping: bool = False
518
+
519
+ def render(self):
520
+ rendered = f"{self.kind} {self.css_selector.render()}"
521
+ if self.from_:
522
+ rendered += f" from {self.from_.render()}"
523
+ if self.within:
524
+ rendered += f" within {self.within.render()}"
525
+ if self.with_wrapping:
526
+ rendered += " with wrapping"
527
+ return rendered
528
+
529
+
530
+ @dataclass
531
+ class ClosestDOM(Expr):
532
+ kind: Literal["closest", "closest parent"]
533
+ css_selector: CSSExpr
534
+ to: Expr | None = None
535
+
536
+ def render(self):
537
+ rendered = f"{self.kind} {self.css_selector.render()}"
538
+ if self.to:
539
+ rendered += f" {self.to.render()}"
540
+ return rendered
541
+
542
+
543
+ type Literals = (
544
+ VariableLiteral
545
+ | CSSExpr.__value__
546
+ | ObjectLiteral
547
+ | ListLiteral
548
+ | NumberLiteral
549
+ | StringLiteral
550
+ | BooleanLiteral
551
+ | NullLiteral
552
+ | TemplateLiteral
553
+ | EventLiteral
554
+ | AttrLiteral
555
+ | Parentheses
556
+ | PropertyAccess
557
+ | PropertyAccessBrackets
558
+ | AsType
559
+ )
560
+
561
+
562
+ ################## COLLECTIONS ##################
563
+ @dataclass
564
+ class PositionalCollection(Expr):
565
+ kind: Literal["first", "last", "random"]
566
+ collection: Expr
567
+ # preposition: Literal["in", "of", "from"] = "in"
568
+
569
+ def render(self):
570
+ return f"{self.kind} {self.collection.render()}"
571
+
572
+
573
+ @dataclass
574
+ class Where(Expr):
575
+ collection: Expr
576
+ condition: Expr
577
+
578
+ def render(self):
579
+ return f"{self.collection.render()} where {self.condition.render()}"
580
+
581
+
582
+ @dataclass
583
+ class MappedTo(Expr):
584
+ collection: Expr
585
+ expr: Expr
586
+
587
+ def render(self):
588
+ return f"{self.collection.render()} mapped to {self.expr.render()}"
589
+
590
+
591
+ ################## PROPERTY ACCESS ##################
592
+ @dataclass
593
+ class PropertyAccess(Expr):
594
+ expr: Expr
595
+ property: str
596
+
597
+ def render(self):
598
+ if isinstance(self.expr, VariableLiteral) and self.expr.symbol == "me":
599
+ return f"my {self.property}"
600
+ if isinstance(self.expr, PropertyAccessBrackets):
601
+ return f"{self.expr.render()}.{self.property}"
602
+ return f"{self.expr.render()}'s {self.property}"
603
+
604
+
605
+ @dataclass
606
+ class PropertyAccessBrackets(Expr):
607
+ expr: Expr
608
+ property: Expr | str
609
+
610
+ def render(self):
611
+ rendered_property = (
612
+ self.property.render()
613
+ if isinstance(self.property, Expr)
614
+ else repr(self.property)
615
+ )
616
+ return f"{self.expr.render()}[{rendered_property}]"
617
+
618
+
619
+ ################## OPS ##################
620
+
621
+ type MATH_BINARY_OPS = Literal["add", "subtract", "multiply", "divide", "mod"]
622
+ type LOGICAL_BINARY_OPS = Literal["and", "or"]
623
+ type BOOL_BINARY_OPS = Literal[
624
+ "eq",
625
+ "ne",
626
+ "gt",
627
+ "gte",
628
+ "lt",
629
+ "lte",
630
+ "matches",
631
+ "does not match",
632
+ "is",
633
+ "is not",
634
+ "is really",
635
+ "is not really",
636
+ "contains",
637
+ "does not contain",
638
+ "starts with",
639
+ "ends with",
640
+ "precedes",
641
+ "does not precede",
642
+ "follows",
643
+ "does not follow",
644
+ "in", # this can be used as a location restrictor for DOM literals
645
+ ]
646
+
647
+
648
+ @dataclass
649
+ class BinaryOp(Expr):
650
+ kind: MATH_BINARY_OPS | LOGICAL_BINARY_OPS | BOOL_BINARY_OPS
651
+ left: Expr
652
+ right: Expr
653
+
654
+ def render(self):
655
+ op = None
656
+ match self.kind:
657
+ case "add":
658
+ op = "+"
659
+ case "subtract":
660
+ op = "-"
661
+ case "multiply":
662
+ op = "*"
663
+ case "lt":
664
+ op = "<"
665
+ case "lte":
666
+ op = "<="
667
+ case "gt":
668
+ op = ">"
669
+ case "gte":
670
+ op = ">="
671
+ case "eq":
672
+ op = "=="
673
+ case "ne":
674
+ op = "!="
675
+ case _:
676
+ op = self.kind
677
+ return f"{self.left.render()} {op} {self.right.render()}"
678
+
679
+
680
+ # MATH_Unary_OPS = ...
681
+ LOGICAL_Unary_OPS = ...
682
+ BOOL_Unary_OPS = Literal[
683
+ "ignoring case", "exists", "does not exist", "is empty", "is not empty"
684
+ ]
685
+
686
+
687
+ @dataclass
688
+ class UnaryOp(Expr):
689
+ pass
690
+
691
+
692
+ ################## PARENTHESES ##################
693
+
694
+
695
+ @dataclass
696
+ class Parentheses(Expr):
697
+ expr: Expr
698
+
699
+ def render(self):
700
+ return f"({self.expr.render()})"
701
+
702
+
703
+ ################## TYPE CONVERSION ##################
704
+ @dataclass
705
+ class TypeConversion(Expr):
706
+ expr: Expr
707
+ type: Literal["JSON", "JSONString"] | Expr
708
+
709
+ def render(self):
710
+ return f"{self.expr.render()} as {self.type.render() if isinstance(self.type, Expr) else self.type}"
711
+
712
+
713
+ @dataclass
714
+ class AsType(Expr):
715
+ expr: Expr
716
+ type: Literal["JSON", "JSONString"]
717
+
718
+ def render(self):
719
+ return f"{python_to_hs(self.expr)} as {self.type}"