···11+# `atplex`
22+33+Name open for discussion; I just needed one but didn't want to think, so i took something where you don't need to think much.
44+55+## Idea
66+77+A low-level python package for interacting with atproto data,
88+enableing you to use typed python to communicate with XRPC endpoints
99+using custom lexicons found on the web.
1010+1111+## What I want it to look in the end
1212+1313+something like this
1414+(except I don't know yet if I want to make it sync or asnyc)
1515+1616+```bash
1717+python -m atplex pull com.example.getProfile
1818+```
1919+2020+```python
2121+import atplex
2222+import lex
2323+2424+client = atplex.Client()
2525+2626+data = await client.exec(lex.com.example.getProfile(user="jojojux.de"))
2727+print(data) # com.example.getProfile#main/output(did=DID("did:plc:f3f3dvty36ztjdqqyxfqhw3p"), name="jojojux.de", displayName=None)
2828+```
2929+3030+## Roadmap
3131+3232+- [ ] Implement lexicon string formats as custom types
3333+ - [x] NSID
3434+ - [x] TID
3535+ - [x] DID
3636+ - [ ] CID
3737+ - [ ] All the others
3838+- [x] Object based representation of lexicons
3939+- [x] Read lexicons into correct object representation
4040+- [x] Codegen from lexicon object representation
4141+ - [x] `None` (partial, but wont-fix) (specifically: cannot be top-level type of a fragment)
4242+ - [x] `bool`, `int`, `str`, `bytes`
4343+ - [ ] `object` (partial)
4444+ - [ ] `list` (partial)
4545+ - [ ] `query`
4646+ - [ ] `procedure`
4747+ - [ ] `params`
4848+ - [ ] `record`
4949+ - [ ] `subscription`
5050+- [ ] Cache lexicon object representation for codegen-ed lexicons
5151+- [ ] Validate data against a lexicon object representation
5252+- [ ] Read data from known lexicons
5353+- [ ] Read data from unknown lexicons
5454+ Decide:
5555+ - Generate lexicon object representation on the fly?
5656+ - Auto resolve lexicons?
5757+- [ ] CLI Tool to resolve & pull lexicons, do codegen, and more
5858+ Idea is essentially sth like a package manager for lexicons
5959+- [ ] Basic XRPC Client
6060+- [ ] Figure out how to best store data that has to conform to the data model without all these fancy-custom types that lexicons have.
6161+- [ ] Improve all of the before
6262+- [ ] I don't know what
···11+import abc
22+import base64
33+from typing import Any
44+from ..id.cid import CIDLink
55+from ..id import NSID, TID, DID
66+77+88+class _Empty: ...
99+1010+1111+Empty = _Empty()
1212+1313+type JSONObject = (
1414+ None | int | bool | str | list[JSONObject] | dict[str, JSONObject] | Empty
1515+)
1616+1717+1818+class SchemaObject[T: Any](abc.ABC):
1919+ @abc.abstractmethod
2020+ def __init__(self, obj: dict[str, JSONObject]): ...
2121+2222+ @abc.abstractmethod
2323+ def serialize(self, obj: T) -> JSONObject: ...
2424+2525+ @abc.abstractmethod
2626+ def deserialize(self, obj: JSONObject) -> T: ...
2727+2828+ @abc.abstractmethod
2929+ def validate_json(self, obj: JSONObject) -> bool: ...
3030+3131+ @abc.abstractmethod
3232+ def validate(self, obj: T) -> bool: ...
3333+3434+3535+class LNull(SchemaObject[None]):
3636+ __lexicon_type__: str = "null"
3737+3838+ def __init__(self, obj: dict[str, JSONObject]):
3939+ if not isinstance(obj, dict):
4040+ raise TypeError("Lexicon typedef must be object")
4141+ assert obj.get("type") == "null"
4242+4343+ def serialize(self, obj: None) -> JSONObject:
4444+ return None
4545+4646+ def deserialize(self, obj: JSONObject) -> None:
4747+ return None
4848+4949+ def validate_json(self, obj: JSONObject) -> bool:
5050+ return obj is None
5151+5252+ def validate(self, obj: None) -> bool:
5353+ return obj is None
5454+5555+5656+class LBoolean(SchemaObject[bool]):
5757+ __lexicon_type__: str = "boolean"
5858+ default: bool | None = None
5959+ const: bool | None = None
6060+6161+ def __init__(self, obj: dict[str, JSONObject]):
6262+ if not isinstance(obj, dict):
6363+ raise TypeError("Lexicon typedef must be object")
6464+ assert obj.get("type") == "boolean"
6565+ # Default is bool or None
6666+ default = obj.get("default")
6767+ assert isinstance(default, bool) or default is None
6868+ self.default = default
6969+ # Const is bool or None
7070+ const = obj.get("const")
7171+ assert isinstance(const, bool) or const is None
7272+ self.const = const
7373+7474+ def serialize(self, obj: bool | None) -> JSONObject:
7575+ # Const -> Empty
7676+ if self.const is not None:
7777+ assert self.const == obj
7878+ return Empty
7979+ # None -> Default
8080+ if obj is None:
8181+ assert self.default is not None
8282+ return self.default
8383+ assert isinstance(obj, bool)
8484+ return obj
8585+8686+ def deserialize(self, obj: JSONObject) -> bool:
8787+ # Const -> VALUE
8888+ if self.const is not None:
8989+ return self.const
9090+ # None -> Default
9191+ if obj is None:
9292+ assert self.default is not None
9393+ return self.default
9494+ assert isinstance(obj, bool)
9595+ return obj
9696+9797+ def validate_json(self, obj: JSONObject) -> bool:
9898+ # Const: Must be empty
9999+ if self.const is not None:
100100+ # TODO: Must obj be Empty or can it opt be eq const?
101101+ return obj is Empty
102102+ # Empty: Must have default
103103+ if obj is Empty:
104104+ return self.default is not None
105105+ # Must be bool
106106+ return isinstance(obj, bool)
107107+108108+ def validate(self, obj: bool | None) -> bool:
109109+ # Const: Must be eq to const
110110+ if self.const is not None:
111111+ return obj == self.const
112112+ # Must be bool or opt None if default exists
113113+ return isinstance(obj, bool) or (self.default is not None and obj is None)
114114+115115+116116+class LInteger(SchemaObject[int]):
117117+ __lexicon_type__: str = "integer"
118118+ minimum: int | None = None
119119+ maximum: int | None = None
120120+ enum: list[int] | None = None
121121+ default: int | None = None
122122+ const: int | None = None
123123+124124+ def __init__(self, obj: dict[str, JSONObject]):
125125+ if not isinstance(obj, dict):
126126+ raise TypeError("Lexicon typedef must be object")
127127+ assert obj.get("type") == "integer"
128128+ # Default is int or None
129129+ default = obj.get("default")
130130+ assert isinstance(default, int) or default is None
131131+ self.default = default
132132+ # Const is int or None
133133+ const = obj.get("const")
134134+ assert isinstance(const, int) or const is None
135135+ self.const = const
136136+ # Enum is list[int] or None
137137+ enum = obj.get("enum")
138138+ assert (
139139+ isinstance(enum, list) and all(isinstance(element, int) for element in enum)
140140+ ) or enum is None
141141+ # Minimum is int or None
142142+ minimum = obj.get("minimum")
143143+ assert isinstance(minimum, int) or minimum is None
144144+ self.minimum = minimum
145145+ # Maximum is int or None
146146+ maximum = obj.get("maximum")
147147+ assert isinstance(maximum, int) or maximum is None
148148+ self.maximum = maximum
149149+ self.enum = enum # type: ignore # Linter does not see: all(isinstance(element, int) for element in enum)
150150+151151+ def serialize(self, obj: int | None) -> JSONObject:
152152+ # Const -> Empty
153153+ if self.const is not None:
154154+ assert self.const == obj
155155+ return Empty
156156+ # None -> Default
157157+ if obj is None:
158158+ assert self.default is not None
159159+ return self.default
160160+ assert self.validate(obj)
161161+ assert isinstance(obj, int)
162162+ return obj
163163+164164+ def deserialize(self, obj: JSONObject) -> int:
165165+ # Const -> VALUE
166166+ if self.const:
167167+ return self.const
168168+ # None -> Default
169169+ if obj is None:
170170+ assert self.default is not None
171171+ return self.default
172172+ assert self.validate_json(obj)
173173+ assert isinstance(obj, int)
174174+ return obj
175175+176176+ def validate_json(self, obj: JSONObject) -> bool:
177177+ # Const: Must be Empty
178178+ if self.const is not None:
179179+ return obj is Empty
180180+ # Empty: Must have default
181181+ if obj is Empty:
182182+ return self.default is not None
183183+ # If enum, must be included
184184+ if self.enum and obj not in self.enum:
185185+ return False
186186+ # Must be int
187187+ if not isinstance(obj, int):
188188+ return False
189189+ # May not be out of bounds
190190+ if self.minimum is not None and obj < self.minimum:
191191+ return False
192192+ if self.maximum is not None and obj > self.maximum:
193193+ return False
194194+ return True
195195+196196+ def validate(self, obj: int | None) -> bool:
197197+ # Const: Must be eq to const
198198+ if self.const is not None:
199199+ return obj == self.const
200200+ # If enum, must be in enum
201201+ if self.enum and obj not in self.enum:
202202+ return False
203203+ # None: Must have default
204204+ if obj is None:
205205+ return self.default is not None
206206+ # Must be int
207207+ if not isinstance(obj, int):
208208+ return False
209209+ # May not be out of bounds
210210+ if self.minimum is not None and obj < self.minimum:
211211+ return False
212212+ if self.maximum is not None and obj > self.maximum:
213213+ return False
214214+ return True
215215+216216+217217+type StringFormatable = TID | NSID
218218+STRING_FORMATABLE = (TID, NSID)
219219+STRING_FORMATS = {
220220+ "at-identifier": ...,
221221+ "at-uri": ...,
222222+ "cid": ...,
223223+ "datetime": ...,
224224+ "did": DID,
225225+ "handle": ...,
226226+ "nsid": NSID,
227227+ "tid": TID,
228228+ "record-key": ...,
229229+ "uri": ...,
230230+ "language": ...,
231231+}
232232+233233+234234+class LString(SchemaObject[str | StringFormatable]):
235235+ __lexicon_type__: str = "string"
236236+ description: str | None = None
237237+ format: str | None = None
238238+ max_length: int | None = None
239239+ min_length: int | None = None
240240+ maxGraphemes: int | None = None
241241+ minGraphemes: int | None = None
242242+ known_values: list[str] | None = None
243243+ enum: list[str] | None = None
244244+ default: str | None = None
245245+ const: str | None = None
246246+247247+ def __init__(self, obj: dict[str, JSONObject]):
248248+ if not isinstance(obj, dict):
249249+ raise TypeError("Lexicon typedef must be object")
250250+ assert obj.get("type") == "string"
251251+ # Default is str or None
252252+ default = obj.get("default")
253253+ assert isinstance(default, str) or default is None
254254+ self.default = default
255255+ # Const is str or None
256256+ const = obj.get("const")
257257+ assert isinstance(const, str) or const is None
258258+ self.const = const
259259+ # Format is str or None
260260+ format_ = obj.get("format")
261261+ assert isinstance(format_, str) or format_ is None
262262+ self.format = format_
263263+ # Enum is list[str] or None
264264+ enum = obj.get("enum")
265265+ assert (
266266+ isinstance(enum, list) and all(isinstance(element, int) for element in enum)
267267+ ) or enum is None
268268+ self.enum = enum # type: ignore # Linter does not see: all(isinstance(element, str) for element in enum)
269269+ # minLength is int or None
270270+ min_length = obj.get("minLength")
271271+ assert isinstance(min_length, int) or min_length is None
272272+ self.min_length = min_length
273273+ # maxLength is int or None
274274+ max_length = obj.get("maxLength")
275275+ assert isinstance(max_length, int) or max_length is None
276276+ self.max_length = max_length
277277+278278+ def serialize(self, obj: StringFormatable | str | None) -> JSONObject:
279279+ # Const -> Empty
280280+ if self.const is not None:
281281+ assert self.const == obj
282282+ return Empty
283283+ # None -> Default
284284+ if obj is None:
285285+ assert self.default is not None
286286+ return self.default
287287+ assert self.validate(obj)
288288+ if self.format is not None:
289289+ assert isinstance(obj, STRING_FORMATS[self.format])
290290+ obj = str(obj)
291291+ assert isinstance(obj, str)
292292+ return obj
293293+294294+ def deserialize(self, obj: JSONObject) -> str | StringFormatable:
295295+ # Const -> VALUE
296296+ if self.const:
297297+ return self.const
298298+ # None -> Default
299299+ if obj is None:
300300+ assert self.default is not None
301301+ return self.default
302302+ assert self.validate_json(obj)
303303+ assert isinstance(obj, str)
304304+ if self.format is not None:
305305+ obj = STRING_FORMATS[self.format](obj)
306306+ return obj
307307+308308+ def validate_json(self, obj: JSONObject) -> bool:
309309+ # Const: Must be Empty
310310+ if self.const is not None:
311311+ return obj is Empty
312312+ # Empty: Must have default
313313+ if obj is Empty:
314314+ return self.default is not None
315315+ # If enum, must be included
316316+ if self.enum and obj not in self.enum:
317317+ return False
318318+ # Must be str
319319+ if not isinstance(obj, str):
320320+ return False
321321+ # May not be out of bounds
322322+ if self.min_length is not None and len(obj) < self.min_length:
323323+ return False
324324+ if self.max_length is not None and len(obj) > self.max_length:
325325+ return False
326326+ # Can be formatable
327327+ if self.format is not None:
328328+ # try:
329329+ # TODO: Improve this ugly thind
330330+ STRING_FORMATS[self.format](obj)
331331+ # except Exception:
332332+ # return False
333333+ return True
334334+335335+ def validate(self, obj: StringFormatable | str | None) -> bool:
336336+ # Const: Must be eq to const
337337+ if self.const is not None:
338338+ return obj == self.const
339339+ # If enum, must be in enum
340340+ if self.enum and obj not in self.enum:
341341+ return False
342342+ # None: Must have default
343343+ if obj is None:
344344+ return self.default is not None
345345+ # Can be formatable
346346+ if self.format is not None:
347347+ if not isinstance(obj, STRING_FORMATS[self.format]):
348348+ return False
349349+ obj = str(obj)
350350+ # Must be str
351351+ if not isinstance(obj, str):
352352+ return False
353353+ # May not be out of bounds
354354+ if self.min_length is not None and len(obj) < self.min_length:
355355+ return False
356356+ if self.max_length is not None and len(obj) > self.max_length:
357357+ return False
358358+ return True
359359+360360+361361+class LBytes(SchemaObject[bytes]):
362362+ __lexicon_type__: str = "bytes"
363363+ description: str | None = None
364364+ format: str | None = None
365365+ max_length: int | None = None
366366+ min_length: int | None = None
367367+368368+ def __init__(self, obj: dict[str, JSONObject]):
369369+ if not isinstance(obj, dict):
370370+ raise TypeError("Lexicon typedef must be object")
371371+ assert obj.get("type") == "bytes"
372372+ # minLength is int or None
373373+ min_length = obj.get("minLength")
374374+ assert isinstance(min_length, int) or min_length is None
375375+ self.min_length = min_length
376376+ # maxLength is int or None
377377+ max_length = obj.get("maxLength")
378378+ assert isinstance(max_length, int) or max_length is None
379379+ self.max_length = max_length
380380+381381+ def serialize(self, obj: bytes) -> JSONObject:
382382+ assert self.min_length is None or len(obj) >= self.min_length
383383+ assert self.max_length is None or len(obj) <= self.max_length
384384+ return {"$bytes": base64.b64encode(obj).decode("ascii")}
385385+386386+ def deserialize(self, obj: JSONObject) -> bytes:
387387+ assert isinstance(obj, dict)
388388+ assert len(obj.keys()) == 1
389389+ vobj = obj.get("$bytes")
390390+ assert isinstance(vobj, str)
391391+ bobj = base64.b64decode(vobj)
392392+ # May not be out of bounds
393393+ assert self.min_length is None or len(bobj) >= self.min_length
394394+ assert self.max_length is None or len(bobj) <= self.max_length
395395+ return bobj
396396+397397+ def validate_json(self, obj: JSONObject) -> bool:
398398+ if not isinstance(obj, dict):
399399+ return False
400400+ if len(obj.keys()) != 1:
401401+ return False
402402+ vobj = obj.get("$bytes")
403403+ if not isinstance(vobj, str):
404404+ return False
405405+ bobj = base64.b64decode(vobj)
406406+ # May not be out of bounds
407407+ if self.min_length is not None and len(bobj) < self.min_length:
408408+ return False
409409+ if self.max_length is not None and len(bobj) > self.max_length:
410410+ return False
411411+ return True
412412+413413+ def validate(self, obj: bytes | None) -> bool:
414414+ if not isinstance(obj, bytes):
415415+ return False
416416+ # May not be out of bounds
417417+ if self.min_length is not None and len(obj) < self.min_length:
418418+ return False
419419+ if self.max_length is not None and len(obj) > self.max_length:
420420+ return False
421421+ return True
422422+423423+424424+class LArray[T](SchemaObject[list[SchemaObject[T]]]):
425425+ __lexicon_type__: str = "array"
426426+ items: SchemaObject[T]
427427+ description: str | None = None
428428+ min_length: int | None = None
429429+ max_length: int | None = None
430430+431431+ def __init__(self, obj: dict[str, JSONObject]):
432432+ if not isinstance(obj, dict):
433433+ raise TypeError("Lexicon typedef must be object")
434434+ assert obj.get("type") == "array"
435435+ # minLength is int or None
436436+ min_length = obj.get("minLength")
437437+ assert isinstance(min_length, int) or min_length is None
438438+ self.min_length = min_length
439439+ # maxLength is int or None
440440+ max_length = obj.get("maxLength")
441441+ assert isinstance(max_length, int) or max_length is None
442442+ self.max_length = max_length
443443+ # items_obj is SchemaObject
444444+ items = obj.get("items")
445445+ assert isinstance(items, dict)
446446+ items_obj = LexiconObject(items)
447447+ self.items = items_obj # type: ignore # Type Hinter is dumb
448448+449449+ def serialize(self, obj: list | None) -> JSONObject:
450450+ assert isinstance(obj, list)
451451+ assert self.validate(obj)
452452+ return [self.items.serialize(element) for element in obj]
453453+454454+ def deserialize(self, obj: JSONObject) -> list:
455455+ assert self.validate_json(obj)
456456+ assert isinstance(obj, list)
457457+ return [self.items.deserialize(element) for element in obj]
458458+459459+ def validate_json(self, obj: JSONObject) -> bool:
460460+ # Must be list
461461+ if not isinstance(obj, list):
462462+ return False
463463+ # validate childs
464464+ if not all(self.items.validate_json(element) for element in obj):
465465+ return False
466466+ # May not be out of bounds
467467+ if self.min_length is not None and len(obj) < self.min_length:
468468+ return False
469469+ if self.max_length is not None and len(obj) > self.max_length:
470470+ return False
471471+ return True
472472+473473+ def validate(self, obj: list) -> bool:
474474+ # Must be str
475475+ if not isinstance(obj, list):
476476+ return False
477477+ # validate childs
478478+ if not all(self.items.validate(element) for element in obj):
479479+ return False
480480+ # May not be out of bounds
481481+ if self.min_length is not None and len(obj) < self.min_length:
482482+ return False
483483+ if self.max_length is not None and len(obj) > self.max_length:
484484+ return False
485485+ return True
486486+487487+488488+class LObject[T](SchemaObject[dict[str, SchemaObject[T]]]):
489489+ __lexicon_type__: str = "object"
490490+ properties: dict[str, "SchemaObject[T]"]
491491+ description: str | None = None
492492+ required: list[str] | None = None
493493+ nullable: list[str] | None = None
494494+495495+ def __init__(self, obj: dict[str, JSONObject]):
496496+ if not isinstance(obj, dict):
497497+ raise TypeError("Lexicon typedef must be object")
498498+ assert obj.get("type") == "object"
499499+ # required is list[str] or None
500500+ required = obj.get("required")
501501+ if required is not None:
502502+ assert isinstance(required, list)
503503+ assert all(isinstance(key, str) for key in required)
504504+ self.required = required # type: ignore # Again
505505+ # nullable is list[str] or None
506506+ nullable = obj.get("nullable")
507507+ if nullable is not None:
508508+ assert isinstance(nullable, list)
509509+ assert all(isinstance(key, str) for key in nullable)
510510+ self.nullable = nullable # type: ignore # And once again
511511+ # properties_obj is dict[str, SchemaObject]
512512+ properties = obj.get("properties")
513513+ assert isinstance(properties, dict)
514514+ assert all(
515515+ isinstance(key, str) and isinstance(value, dict)
516516+ for key, value in properties.items()
517517+ )
518518+ properties_obj = {
519519+ key: LexiconObject(value) # type: ignore # I hate this
520520+ for key, value in properties.items()
521521+ }
522522+ self.properties = properties_obj
523523+524524+ def serialize(self, obj: dict | None) -> JSONObject:
525525+ assert isinstance(obj, dict)
526526+ assert self.validate(obj)
527527+ return {
528528+ key: None
529529+ if value is None and self.nullable is not None and key in self.nullable
530530+ else self.properties[key].serialize(value)
531531+ for key, value in obj.items()
532532+ }
533533+534534+ def deserialize(self, obj: JSONObject) -> dict:
535535+ assert isinstance(obj, dict)
536536+ assert self.validate_json(obj)
537537+ return {
538538+ key: None
539539+ if value is None and self.nullable is not None and key in self.nullable
540540+ else self.properties[key].deserialize(value)
541541+ for key, value in obj.items()
542542+ }
543543+544544+ def validate_json(self, obj: JSONObject) -> bool:
545545+ # Must be dict
546546+ if not isinstance(obj, dict):
547547+ return False
548548+ if self.required is not None:
549549+ for key in self.required:
550550+ if obj.get("key", Empty) is Empty:
551551+ return False
552552+ for key in self.properties:
553553+ if obj.get(key, Empty) is Empty:
554554+ continue
555555+ if self.nullable is not None and key in self.nullable and obj[key] is None:
556556+ continue
557557+558558+ if not self.properties[key].validate_json(obj[key]):
559559+ return False
560560+ return True
561561+562562+ def validate(self, obj: dict) -> bool:
563563+ # Must be dict
564564+ if not isinstance(obj, dict):
565565+ return False
566566+ if self.required is not None:
567567+ for key in self.required:
568568+ if key not in obj:
569569+ return False
570570+ for key in self.properties:
571571+ if key not in obj:
572572+ continue
573573+ if self.nullable is not None and key in self.nullable and obj[key] is None:
574574+ continue
575575+576576+ if not self.properties[key].validate(obj[key]):
577577+ print(key, "is invalid")
578578+ return False
579579+ return True
580580+581581+582582+class LParams[T](SchemaObject[dict[str, SchemaObject[T]]]):
583583+ __lexicon_type__: str = "params"
584584+ properties: dict[str, "SchemaObject[T]"]
585585+ description: str | None = None
586586+ required: list[str] | None = None
587587+588588+ def __init__(self, obj: dict[str, JSONObject]):
589589+ if not isinstance(obj, dict):
590590+ raise TypeError("Lexicon typedef must be object")
591591+ assert obj.get("type") == "params"
592592+ # required is list[str] or None
593593+ required = obj.get("required")
594594+ if required is not None:
595595+ assert isinstance(required, list)
596596+ assert all(isinstance(key, str) for key in required)
597597+ self.required = required # type: ignore # Again
598598+ # properties_obj is dict[str, SchemaObject]
599599+ properties = obj.get("properties")
600600+ assert isinstance(properties, dict)
601601+ assert all(
602602+ isinstance(key, str)
603603+ and isinstance(value, dict)
604604+ and value.get("type")
605605+ in ("string", "integer", "boolean", "unknown") # TODO: array
606606+ for key, value in properties.items()
607607+ )
608608+ properties_obj = {
609609+ key: LexiconObject(value) # type: ignore # I hate this
610610+ for key, value in properties.items()
611611+ }
612612+ self.properties = properties_obj
613613+614614+ def serialize(self, obj: dict | None) -> JSONObject:
615615+ assert isinstance(obj, dict)
616616+ assert self.validate(obj)
617617+ return {
618618+ key: self.properties[key].serialize(value) for key, value in obj.items()
619619+ }
620620+621621+ def deserialize(self, obj: JSONObject) -> dict:
622622+ assert isinstance(obj, dict)
623623+ assert self.validate_json(obj)
624624+ return {
625625+ key: self.properties[key].deserialize(value) for key, value in obj.items()
626626+ }
627627+628628+ def validate_json(self, obj: JSONObject) -> bool:
629629+ # Must be dict
630630+ if not isinstance(obj, dict):
631631+ return False
632632+ if self.required is not None:
633633+ for key in self.required:
634634+ if obj.get("key", Empty) is Empty:
635635+ return False
636636+ for key in self.properties:
637637+ if obj.get(key, Empty) is Empty:
638638+ continue
639639+640640+ if not self.properties[key].validate_json(obj[key]):
641641+ return False
642642+ return True
643643+644644+ def validate(self, obj: dict) -> bool:
645645+ # Must be dict
646646+ if not isinstance(obj, dict):
647647+ return False
648648+ if self.required is not None:
649649+ for key in self.required:
650650+ if key not in obj:
651651+ return False
652652+ for key in self.properties:
653653+ if key not in obj:
654654+ continue
655655+656656+ if not self.properties[key].validate(obj[key]):
657657+ print(key, "is invalid")
658658+ return False
659659+ return True
660660+661661+662662+TYPE_MAP: dict[str, type[SchemaObject]] = {
663663+ "null": LNull,
664664+ "boolean": LBoolean,
665665+ "integer": LInteger,
666666+ "string": LString,
667667+ "bytes": LBytes,
668668+ "array": LArray,
669669+ "object": LObject,
670670+ "params": LParams,
671671+}
672672+673673+674674+def LexiconObject(obj: dict[str, JSONObject]):
675675+ type_ = obj.get("type")
676676+ if type_ not in TYPE_MAP:
677677+ raise TypeError(f"Cannot find lexcion type '{type_}'")
678678+ return TYPE_MAP[type_](obj)
···11+ALPHABET = "234567abcdefghijklmnopqrstuvwxyz"
22+DECODING_MAP = {v: k for k, v in enumerate(ALPHABET)}
33+44+55+def encode_int(data: int, length: int) -> str:
66+ encoded = ""
77+ for i in range(0, length, 5):
88+ c = data & 0b11111
99+ data >>= 5
1010+ encoded += ALPHABET[c]
1111+ return "".join(reversed(encoded))
1212+1313+1414+def decode_int(data: str) -> int:
1515+ decoded = 0
1616+ for letter in data:
1717+ if letter not in DECODING_MAP:
1818+ raise ValueError("Non-base32-sortable digit found")
1919+2020+ decoded <<= 5
2121+ decoded += DECODING_MAP[letter]
2222+ return decoded
+7
src/id/__init__.py
···11+from .did import DID
22+from .nsid import NSID, NSIDGlob
33+from .tid import TID
44+55+type StringableIdentifier = DID | NSID | TID
66+77+__all__ = ["DID", "NSID", "NSIDGlob", "TID", "StringableIdentifier"]