hassl 0.3.1__py3-none-any.whl → 0.4.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.
@@ -1,8 +1,10 @@
1
- from dataclasses import dataclass, field
1
+ from dataclasses import dataclass, field, is_dataclass, asdict
2
2
  from typing import Dict, List, Any, Optional, Tuple
3
+ import copy
3
4
  from ..ast.nodes import (
4
5
  Program, Alias, Sync, Rule, Schedule,
5
6
  HolidaySet, ScheduleWindow, PeriodSelector,
7
+ TemplateDecl, UseTemplate,
6
8
  )
7
9
  from .domains import DOMAIN_PROPS, domain_of
8
10
 
@@ -60,6 +62,20 @@ class IRProgram:
60
62
  "schedules_windows": self.schedules_windows or {}, # NEW
61
63
  }
62
64
 
65
+ def _resolve_module_id(raw_mod: str) -> str:
66
+ """
67
+ Map a raw import like 'hall.aliases' to the actual package id present in GLOBAL_EXPORTS,
68
+ e.g. 'home.hall.aliases'. If multiple candidates match, keep raw (fail gently).
69
+ """
70
+ if 'GLOBAL_EXPORTS' not in globals() or not raw_mod:
71
+ return raw_mod
72
+ # exact hit?
73
+ if any(pkg == raw_mod for (pkg, _k, _n) in globals()['GLOBAL_EXPORTS']):
74
+ return raw_mod
75
+ # suffix match (common when files declare 'home.hall.aliases' but source wrote 'hall.aliases')
76
+ candidates = {pkg for (pkg, _k, _n) in globals()['GLOBAL_EXPORTS'] if pkg.endswith("." + raw_mod)}
77
+ return next(iter(candidates)) if len(candidates) == 1 else raw_mod
78
+
63
79
  def _resolve_alias(e: str, amap: Dict[str,str]) -> str:
64
80
  if "." not in e and e in amap: return amap[e]
65
81
  return e
@@ -114,8 +130,148 @@ def analyze(prog: Program) -> IRProgram:
114
130
  local_schedule_windows: Dict[str, List[ScheduleWindow]] = {}
115
131
  local_holidays: Dict[str, HolidaySet] = {}
116
132
 
117
- # 1) Collect local declarations (aliases & schedules)
118
- for s in prog.statements:
133
+ # 0) Preprocess: collect templates and expand 'use template' into concrete nodes
134
+ templates_by_kind: Dict[str, Dict[str, TemplateDecl]] = {"rule": {}, "sync": {}, "schedule": {}}
135
+
136
+ # Collect modules this program imports (treat bare "import X" as glob, same as you already do)
137
+ imported_modules = set()
138
+ for imp in getattr(prog, "imports", []) or []:
139
+ if not isinstance(imp, dict) or imp.get("type") != "import":
140
+ continue
141
+ raw_mod = imp.get("module", "")
142
+ mod = _resolve_module_id(raw_mod)
143
+
144
+ if not mod:
145
+ continue
146
+ # normalize kind the same way you do later
147
+ kind = imp.get("kind")
148
+ if kind not in ("glob", "list", "alias"):
149
+ if imp.get("items"): kind = "list"
150
+ elif imp.get("as"): kind = "alias"
151
+ else: kind = "glob"
152
+
153
+ if kind in ("glob", "list", "alias"):
154
+ imported_modules.add(mod)
155
+
156
+ # Helper: build arg map (params -> values) using defaults
157
+ def _bind_args(t: TemplateDecl, call_args: List[Any]) -> Dict[str, Any]:
158
+ params = list(t.params or [])
159
+ # normalize params: [{"name":..., "default":...}, ...]
160
+ pnames = [p.get("name") for p in params]
161
+ defaults = {p.get("name"): p.get("default") for p in params}
162
+ bound: Dict[str, Any] = dict(defaults)
163
+ # split named vs positional args from transformer
164
+ pos: List[Any] = []
165
+ named: Dict[str, Any] = {}
166
+ for a in call_args or []:
167
+ if isinstance(a, dict) and "name" in a:
168
+ named[str(a["name"])] = a.get("value")
169
+ else:
170
+ pos.append(a)
171
+ # apply positional
172
+ for i, v in enumerate(pos):
173
+ if i < len(pnames):
174
+ bound[pnames[i]] = v
175
+ # apply named (wins over positional/default)
176
+ for k, v in named.items():
177
+ if k in pnames:
178
+ bound[k] = v
179
+ return bound
180
+
181
+ # Deep substitute param identifiers appearing as bare strings in nested dict/list trees
182
+ def _deep_subst(obj: Any, subst: Dict[str, Any]) -> Any:
183
+ # strings: replace only if exactly matches a parameter name
184
+ if isinstance(obj, str):
185
+ return str(subst.get(obj, obj))
186
+ # dicts/lists: walk recursively
187
+ if isinstance(obj, dict):
188
+ return {k: _deep_subst(v, subst) for k, v in obj.items()}
189
+ if isinstance(obj, list):
190
+ return [_deep_subst(x, subst) for x in obj]
191
+ # leave everything else as-is (numbers, bools, None)
192
+ return obj
193
+
194
+ # Expand a single UseTemplate into a concrete node (Rule/Sync/Schedule)
195
+ def _instantiate(use: UseTemplate) -> Optional[Any]:
196
+ # Find matching template by name across kinds (prefer rule->sync->schedule)
197
+ t = None
198
+ for kind in ("rule", "sync", "schedule"):
199
+ t = templates_by_kind.get(kind, {}).get(use.name)
200
+ if t:
201
+ break
202
+ if not t:
203
+ return None
204
+ argmap = _bind_args(t, list(getattr(use, "args", []) or []))
205
+
206
+ if t.body is None:
207
+ # Provide minimal empty bodies so deep_subst & constructors don’t break
208
+ if t.kind == "rule":
209
+ t.body = Rule(name="", clauses=[])
210
+ elif t.kind == "sync":
211
+ t.body = Sync(kind="onoff", members=[], name="", invert=[])
212
+ elif t.kind == "schedule":
213
+ t.body = Schedule(name="", clauses=[], windows=[], private=False)
214
+ original = copy.deepcopy(t.body)
215
+ # Plainify dataclasses/objects so deep_subst can walk them
216
+ if is_dataclass(original):
217
+ plain = asdict(original)
218
+ elif hasattr(original, "__dict__"):
219
+ # shallow mapping of fields; they will typically be lists/dicts we can walk
220
+ plain = copy.deepcopy(vars(original))
221
+ else:
222
+ plain = original
223
+ subbed = _deep_subst(plain, argmap)
224
+
225
+ # Rename resulting node if caller provided "as <name>"
226
+ # Rename resulting node if caller provided "as <name>" or passed name= param
227
+ new_name = getattr(use, "as_name", None) or str(argmap.get("name") or t.name)
228
+
229
+ # Construct concrete AST node of same kind
230
+ if isinstance(t.body, Rule) or (getattr(t.body, "__class__", None).__name__ == "Rule"):
231
+ # subbed is a dict after plainify/subst
232
+ return Rule(name=new_name, clauses=subbed.get("clauses", getattr(original, "clauses", [])))
233
+ if isinstance(t.body, Sync) or (getattr(t.body, "__class__", None).__name__ == "Sync"):
234
+ return Sync(kind=subbed.get("kind", getattr(original, "kind", "onoff")),
235
+ members=subbed.get("members", getattr(original, "members", [])),
236
+ name=new_name,
237
+ invert=subbed.get("invert", getattr(original, "invert", [])))
238
+ if isinstance(t.body, Schedule) or (getattr(t.body, "__class__", None).__name__ == "Schedule"):
239
+ return Schedule(name=new_name,
240
+ clauses=subbed.get("clauses", getattr(original, "clauses", [])),
241
+ windows=subbed.get("windows", getattr(original, "windows", [])),
242
+ private=subbed.get("private", getattr(original, "private", False)))
243
+ return None
244
+
245
+ # Scan once to collect templates
246
+ for s in getattr(prog, "statements", []) or []:
247
+ if isinstance(s, TemplateDecl):
248
+ kind = (s.kind or "rule").lower()
249
+ if kind in templates_by_kind:
250
+ templates_by_kind[kind][s.name] = s
251
+
252
+ if 'GLOBAL_EXPORTS' in globals():
253
+ for (pkg, kind, name), node in globals()['GLOBAL_EXPORTS'].items():
254
+ if kind != "template":
255
+ continue
256
+ # bring templates from any imported module
257
+ if pkg in imported_modules and isinstance(node, TemplateDecl):
258
+ tkind = (node.kind or "rule").lower()
259
+ if tkind in templates_by_kind and name not in templates_by_kind[tkind]:
260
+ templates_by_kind[tkind][name] = node
261
+
262
+ # Build a new statement list with uses expanded
263
+ expanded_statements: List[Any] = []
264
+ for s in getattr(prog, "statements", []) or []:
265
+ if isinstance(s, UseTemplate):
266
+ inst = _instantiate(s)
267
+ if inst is not None:
268
+ expanded_statements.append(inst)
269
+ # do not append the UseTemplate node itself
270
+ else:
271
+ expanded_statements.append(s)
272
+
273
+ # 1) Collect local declarations (aliases & schedules) from expanded statements
274
+ for s in expanded_statements:
119
275
  if isinstance(s, Alias):
120
276
  local_aliases[s.name] = s.entity
121
277
  is_private = getattr(s, "private", False)
@@ -173,7 +329,9 @@ def analyze(prog: Program) -> IRProgram:
173
329
  if not isinstance(imp, dict) or imp.get("type") != "import":
174
330
  # transformer may also append sentinels to statements; ignore here
175
331
  continue
176
- mod = imp.get("module", "")
332
+ raw_mod = imp.get("module", "")
333
+ mod = _resolve_module_id(raw_mod)
334
+
177
335
  kind = imp.get("kind")
178
336
  # Be generous: if transformer emitted "none" or omitted kind, infer it.
179
337
  if kind not in ("glob", "list", "alias"):
@@ -224,7 +382,7 @@ def analyze(prog: Program) -> IRProgram:
224
382
 
225
383
  # --- Syncs ---
226
384
  syncs: List[IRSync] = []
227
- for s in prog.statements:
385
+ for s in expanded_statements:
228
386
  if isinstance(s, Sync):
229
387
  mem = [_resolve_alias(m,amap) for m in s.members]
230
388
  inv = [_resolve_alias(m,amap) for m in s.invert]
@@ -239,6 +397,33 @@ def analyze(prog: Program) -> IRProgram:
239
397
 
240
398
  # NEW: collect structured windows (serialize to plain dicts)
241
399
  sched_windows: Dict[str, List[dict]] = {}
400
+
401
+ # --- helpers: normalization for day selector & holiday mode ---
402
+ def _norm_day_selector(ds: Optional[str]) -> str:
403
+ s = (ds or "").strip().lower()
404
+ if s in ("weekdays", "weekday", "wd", "mon-fri", "monfri"):
405
+ return "weekdays"
406
+ if s in ("weekends", "weekend", "we", "sat-sun", "satsun"):
407
+ return "weekends"
408
+ return "daily"
409
+
410
+ def _norm_holiday_mode(mode: Optional[str]) -> Optional[str]:
411
+ """
412
+ Normalize holiday text to {'only','except',None}.
413
+ Accepts variants like:
414
+ 'holiday', 'only holiday', 'holiday only' -> 'only'
415
+ 'except holiday', 'exclude holiday', 'unless holiday', 'not holiday' -> 'except'
416
+ """
417
+ if mode is None:
418
+ return None
419
+ m = str(mode).strip().lower().replace("_", " ").replace("-", " ")
420
+ # look for negation/exclusion first
421
+ if any(tok in m for tok in ("except", "exclude", "unless", "not")):
422
+ return "except"
423
+ if "holiday" in m or "only" in m:
424
+ return "only"
425
+ return None
426
+
242
427
  for nm, wins in local_schedule_windows.items():
243
428
  out: List[dict] = []
244
429
  for w in wins:
@@ -249,14 +434,23 @@ def analyze(prog: Program) -> IRProgram:
249
434
  if getattr(w, "period", None):
250
435
  p = w.period # PeriodSelector
251
436
  period = {"kind": p.kind, "data": dict(p.data)}
437
+
438
+ # --- normalize selectors & holiday mode ---
439
+ day_sel = _norm_day_selector(getattr(w, "day_selector", None))
440
+ href = getattr(w, "holiday_ref", None)
441
+ hmode = _norm_holiday_mode(getattr(w, "holiday_mode", None))
442
+ # Heuristic default: if a weekdays/weekends selector references a holiday
443
+ # set and no mode provided, treat as "except" (workday semantics).
444
+ if href and hmode is None and day_sel in ("weekdays", "weekends"):
445
+ hmode = "except"
252
446
  out.append({
253
447
  "start": w.start,
254
448
  "end": w.end,
255
- "day_selector": w.day_selector,
449
+ "day_selector": day_sel,
256
450
  "period": period,
257
- "holiday_ref": w.holiday_ref,
258
- "holiday_mode": w.holiday_mode,
259
- })
451
+ "holiday_ref": href,
452
+ "holiday_mode": hmode,
453
+ })
260
454
  if out:
261
455
  sched_windows[nm] = out
262
456
 
@@ -338,8 +532,22 @@ def analyze(prog: Program) -> IRProgram:
338
532
  if ent:
339
533
  return ent
340
534
  return obj
535
+
536
+ # Resolve only qualified aliases (ns.alias) and leave unqualified aliases intact.
537
+ # This keeps existing IR expectations while making qualified references usable.
538
+ def _walk_qualified_only(obj: Any) -> Any:
539
+ if isinstance(obj, dict):
540
+ return {k: _walk_qualified_only(v) for k, v in obj.items()}
541
+ if isinstance(obj, list):
542
+ return [_walk_qualified_only(x) for x in obj]
543
+ if isinstance(obj, str):
544
+ if "." in obj:
545
+ ent = _resolve_qualified_alias(obj)
546
+ if ent:
547
+ return ent
548
+ return obj
341
549
 
342
- for s in prog.statements:
550
+ for s in expanded_statements:
343
551
  if isinstance(s, Rule):
344
552
  clauses: List[dict] = []
345
553
  schedule_uses: List[str] = []
@@ -351,8 +559,9 @@ def analyze(prog: Program) -> IRProgram:
351
559
  # IfClause-like items have .condition/.actions
352
560
  if hasattr(c, "condition") and hasattr(c, "actions"):
353
561
  # Keep alias identifiers intact for tests & codegen (resolve later)
354
- cond = c.condition
355
- acts = c.actions
562
+ # But resolve qualified aliases (ns.alias) so codegen gets real entities
563
+ cond = _walk_qualified_only(c.condition)
564
+ acts = _walk_qualified_only(c.actions)
356
565
  clauses.append({"condition": cond, "actions": acts})
357
566
  elif isinstance(c, dict) and c.get("type") == "schedule_use":
358
567
  # {"type":"schedule_use","names":[...]}
@@ -371,6 +580,10 @@ def analyze(prog: Program) -> IRProgram:
371
580
  for sc in c.get("clauses") or []:
372
581
  if isinstance(sc, dict):
373
582
  schedules_inline.append(sc)
583
+ elif isinstance(c, dict) and "condition" in c and "actions" in c:
584
+ cond = _walk_alias_with_qualified(c["condition"])
585
+ acts = _walk_alias_with_qualified(c["actions"])
586
+ clauses.append({"condition": cond, "actions": acts})
374
587
  else:
375
588
  # ignore unknown fragments
376
589
  pass
@@ -386,6 +599,16 @@ def analyze(prog: Program) -> IRProgram:
386
599
  # -------- NEW: validate schedule windows --------
387
600
  allowed_days = {"weekdays", "weekends", "daily"}
388
601
  for sched_name, wins in sched_windows.items():
602
+ # Normalize holiday modes defensively:
603
+ # If a window references a holiday set and also specifies a day bucket,
604
+ # it should be "except" (non-holiday behavior). We keep pure holiday-only
605
+ # windows (`day_selector == "daily"`) as "only".
606
+ for w in wins:
607
+ ds = (w.get("day_selector") or "daily").lower()
608
+ href = w.get("holiday_ref")
609
+ hmode = w.get("holiday_mode")
610
+ if href and ds in ("weekdays", "weekends") and (hmode is None or hmode == "only"):
611
+ w["holiday_mode"] = "except"
389
612
  for w in wins:
390
613
  ds = w.get("day_selector")
391
614
  if ds not in allowed_days:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hassl
3
- Version: 0.3.1
3
+ Version: 0.4.0
4
4
  Summary: HASSL: Home Assistant Simple Scripting Language
5
5
  Home-page: https://github.com/adanowitz/hassl
6
6
  Author: adanowitz
@@ -17,7 +17,7 @@ Dynamic: license-file
17
17
 
18
18
  > **Home Assistant Simple Scripting Language**
19
19
 
20
- ![Version](https://img.shields.io/badge/version-v0.3.1-blue)
20
+ ![Version](https://img.shields.io/badge/version-v0.4.0-blue)
21
21
 
22
22
  HASSL is a human-friendly domain-specific language (DSL) for building **loop-safe**, **deterministic**, and **composable** automations for [Home Assistant](https://www.home-assistant.io/).
23
23
 
@@ -30,7 +30,7 @@ It compiles lightweight `.hassl` scripts into fully functional YAML packages tha
30
30
  - **Readable DSL** → write logic like natural language (`if motion && lux < 50 then light = on`)
31
31
  - **Sync devices** → keep switches, dimmers, and fans perfectly in sync
32
32
  - **Schedules** → declare time-based gates (`enable from 08:00 until 19:00`)
33
- - **Weekday/weekend/holiday schedules** → full support for Home Assistant’s **Workday integration** (v0.3.1)
33
+ - **Weekday/weekend/holiday schedules** → full support for Home Assistant’s **Workday integration** (v0.4.0)
34
34
  - **Loop-safe** → context ID tracking prevents feedback loops
35
35
  - **Per-rule enable gates** → `disable rule` or `enable rule` dynamically
36
36
  - **Inline waits** → `wait (!motion for 10m)` works like native HA triggers
@@ -148,7 +148,7 @@ Each `.hassl` file compiles into an isolated package — no naming collisions, n
148
148
  | `scripts_<pkg>.yaml` | Writer scripts with context stamping |
149
149
  | `sync_<pkg>_*.yaml` | Sync automations for each property |
150
150
  | `rules_bundled_<pkg>.yaml` | Rule logic automations + schedules |
151
- | `schedules_<pkg>.yaml` | Time/sun-based schedule sensors (v0.3.1) |
151
+ | `schedules_<pkg>.yaml` | Time/sun-based schedule sensors (v0.4.0) |
152
152
 
153
153
  ---
154
154
 
@@ -181,7 +181,7 @@ All schedules are restart-safe:
181
181
 
182
182
  ---
183
183
 
184
- ## 🗓️ Holiday & Workday Integration (v0.3.1)
184
+ ## 🗓️ Holiday & Workday Integration (v0.4.0)
185
185
 
186
186
  HASSL now supports `holidays <id>:` schedules tied to Home Assistant’s **Workday** integration.
187
187
 
@@ -238,7 +238,7 @@ Once created, HASSL automatically references them in generated automations.
238
238
 
239
239
  ## ⚗️ Experimental: Date & Month Range Schedules
240
240
 
241
- HASSL v0.3.1 includes early support for:
241
+ HASSL v0.4.0 includes early support for:
242
242
 
243
243
  ```hassl
244
244
  on months Jun–Aug 07:00–22:00;
@@ -0,0 +1,23 @@
1
+ hassl/__init__.py,sha256=42STGor_9nKYXumfeV5tiyD_M8VdcddX7CEexmibPBk,22
2
+ hassl/cli.py,sha256=8y15glAYjHsJChaHefwqvoC5sz-l-pt9E9mWtzrt-Ik,9890
3
+ hassl/ast/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ hassl/ast/nodes.py,sha256=Z6Ac5RrDEv6KNAAxdKQ3wrnnoHqbgP6-SDvda9Z8CNk,3254
5
+ hassl/codegen/__init__.py,sha256=NgEw86oHlsk7cQrHz8Ttrtp68Cm7WKceTWRr02SDfjo,854
6
+ hassl/codegen/generate.py,sha256=JmVBz8xhuPHosch0lhh8xU6tmdCsAl-qVWoY7hQxjow,206
7
+ hassl/codegen/init.py,sha256=kMfi_Us_7c_T35OK_jo8eqb79lxNV-dToO9-iAb5fHI,55
8
+ hassl/codegen/package.py,sha256=wruN26-wo64RBb94SxyEhG_4NIc6zBFcvOhGpuO_CIA,43289
9
+ hassl/codegen/rules_min.py,sha256=3hiL0dUvlU9uIf1kc_BYcyTsaWGUBS787WkaIh1vZwk,28909
10
+ hassl/codegen/yaml_emit.py,sha256=VTNnR_uvSSqsL7kX5NyXuPUZh5FK36a_sUFsRyrQOS8,2207
11
+ hassl/parser/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ hassl/parser/hassl.lark,sha256=aBlZ609bZ-Wnrs5EsFukDqAIo5xLNQ6p2ruS5qmxtI8,6978
13
+ hassl/parser/loader.py,sha256=xwhdoMkmXskanY8IhqIOfkcOkkn33goDnsbjzS66FL8,249
14
+ hassl/parser/transform.py,sha256=xVz1fKjfxHbiTeUFuZJbVwctGWwd4cdvAPhfKgslwbs,25328
15
+ hassl/semantics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
+ hassl/semantics/analyzer.py,sha256=layYd1x_UajNPZN9Up_fp2dfApnBzCyeoG351nutpjU,28641
17
+ hassl/semantics/domains.py,sha256=-OuhA4RkOLVaLnBxhKfJFjiQMYBSTryX9KjHit_8ezI,308
18
+ hassl-0.4.0.dist-info/licenses/LICENSE,sha256=hPNSrn93Btl9sU4BkQlrIK1FpuFPvaemdW6-Scz-Xeo,1072
19
+ hassl-0.4.0.dist-info/METADATA,sha256=Noo52fAx3gT0U5s0hrNMeia8BLnLgXnXsuSkhKpyZZE,9006
20
+ hassl-0.4.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
21
+ hassl-0.4.0.dist-info/entry_points.txt,sha256=qA8jYPZlESNL6V4fJCTPOBXFeEBtjLGGsftXsSo82so,42
22
+ hassl-0.4.0.dist-info/top_level.txt,sha256=BPuiULeikxMI3jDVLmv3_n7yjSD9Qrq58bp9af_HixI,6
23
+ hassl-0.4.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,23 +0,0 @@
1
- hassl/__init__.py,sha256=r4xAFihOf72W9TD-lpMi6ntWSTKTP2SlzKP1ytkjRbI,22
2
- hassl/cli.py,sha256=TSUvkoAg7ck1vlDUhC2sv_1NSetksdVGuOv-P9wrKxA,15762
3
- hassl/ast/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- hassl/ast/nodes.py,sha256=U_UTmWS87laFuFX39mmzkk43JnGx7NEyNbkSomfTzSM,2704
5
- hassl/codegen/__init__.py,sha256=NgEw86oHlsk7cQrHz8Ttrtp68Cm7WKceTWRr02SDfjo,854
6
- hassl/codegen/generate.py,sha256=JmVBz8xhuPHosch0lhh8xU6tmdCsAl-qVWoY7hQxjow,206
7
- hassl/codegen/init.py,sha256=kMfi_Us_7c_T35OK_jo8eqb79lxNV-dToO9-iAb5fHI,55
8
- hassl/codegen/package.py,sha256=sGG40gonOoK3QJphO62Bju6WqFqfTT3XNx37QPHlb2w,42457
9
- hassl/codegen/rules_min.py,sha256=LbLrpJ_2TV_FJlHB17TcuBaqfbLl97VUKzLck3s9KXo,29557
10
- hassl/codegen/yaml_emit.py,sha256=VTNnR_uvSSqsL7kX5NyXuPUZh5FK36a_sUFsRyrQOS8,2207
11
- hassl/parser/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
- hassl/parser/hassl.lark,sha256=-At0mQQiUXxQU2aB1pxrk1WfQdQiUJeH7Ev8ada7kLg,5637
13
- hassl/parser/loader.py,sha256=xwhdoMkmXskanY8IhqIOfkcOkkn33goDnsbjzS66FL8,249
14
- hassl/parser/transform.py,sha256=UXiUQBT3oMkFVJwFWA8X3L_Ds7PzZle7ZMBVVNSmp7Y,26332
15
- hassl/semantics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
- hassl/semantics/analyzer.py,sha256=M8pQTFRYSpAyBo-ctIRuT1DBb_WZsGs7hjT4-pHPLjA,18066
17
- hassl/semantics/domains.py,sha256=-OuhA4RkOLVaLnBxhKfJFjiQMYBSTryX9KjHit_8ezI,308
18
- hassl-0.3.1.dist-info/licenses/LICENSE,sha256=hPNSrn93Btl9sU4BkQlrIK1FpuFPvaemdW6-Scz-Xeo,1072
19
- hassl-0.3.1.dist-info/METADATA,sha256=TYCCs2uU-7mRXyvq2_pkMLbVMQY_VTqW5Oi5i0msaJg,9006
20
- hassl-0.3.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
21
- hassl-0.3.1.dist-info/entry_points.txt,sha256=qA8jYPZlESNL6V4fJCTPOBXFeEBtjLGGsftXsSo82so,42
22
- hassl-0.3.1.dist-info/top_level.txt,sha256=BPuiULeikxMI3jDVLmv3_n7yjSD9Qrq58bp9af_HixI,6
23
- hassl-0.3.1.dist-info/RECORD,,