indikit.protocol¶
The single source of truth for the INDI 1.7 wire format: typed enums and
Pydantic models that serialize to both INDI XML (for indiserver) and JSON
(for browsers).
Enums¶
indikit.protocol.enums ¶
INDI protocol enumerations.
Each enum subclasses :class:enum.StrEnum, so a member is the exact token used
on the INDI wire (e.g. IPState.OK == "Ok" is True) and Pydantic serialises
it to that token directly.
:func:coerce_switch lives here too, because every caller-facing API that takes
a switch value has to accept the same three spellings of it.
IPState ¶
Bases: _StrEnum
State of a vector property (the coloured status light in a GUI).
IPerm ¶
Bases: _StrEnum
Client access permission for a vector property.
ISRule ¶
Bases: _StrEnum
Constraint on how many switches in a switch vector may be On.
ISState ¶
Bases: _StrEnum
On/Off state of a single switch.
BLOBPolicy ¶
Bases: _StrEnum
How indiserver should deliver BLOBs to a client.
A client must send an enableBLOB with one of these policies before
indiserver will forward any BLOB; the default on the wire is Never.
coerce_switch ¶
coerce_switch(value: ISState | bool | str) -> ISState
Coerce a caller-supplied switch value into an :class:ISState.
Every API that takes a switch value from application code - the driver's
BoundProperty.set, the client's set_switch, the test harness's
write - accepts the same three spellings, because POWER=True is what
a caller writes and "On" is what the wire calls it. This is the one
implementation of that rule.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
ISState or bool or str
|
An |
required |
Returns:
| Name | Type | Description |
|---|---|---|
state |
ISState
|
The corresponding switch state. |
Raises:
| Type | Description |
|---|---|
ValueError
|
Raised if a string names no switch state. |
Source code in src/indikit/protocol/enums.py
89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 | |
Models¶
indikit.protocol.models ¶
Typed Pydantic models for INDI properties and wire messages.
Design notes
-
A vector (
NumberVectoretc.) is the canonical, in-memory representation of a property. It carries the full metadata plus its elements. A driver holds vectors; a client caches them and applies incoming updates onto them. -
Element metadata (
format/min/max/stepon numbers,ruleon switch vectors, ...) is only present in adefmessage. Insetandnewmessages the wire carries justname+ value per element. We model that by making the metadata fields optional with defaults, so the same element class round-trips through both contexts. Clients are expected to mergesetvalues onto the previously-defined vector, which is standard INDI behavior. -
The
def/set/newdistinction is a wire intent, not a different data shape, so it is expressed by the thin event wrappers at the bottom of this module rather than by duplicating every vector five times. -
Every timestamp is UTC. INDI requires it (white paper p.5) and libindi writes a bare, offset-less
%Y-%m-%dT%H:%M:%S, so :data:IndiTimestampnormalises whatever a caller or a peer supplies into an aware UTC datetime in one place - the model - rather than at each of the codecs.
Number ¶
Bases: _Element
A single numeric element (defNumber / oneNumber).
value refuses NaN and the infinities. It is the one non-nullable float
on the wire, and JSON has no way to write a non-finite number: the codec
would emit null for it and :func:~indikit.protocol.json.from_json
would then reject its own output, because value is required. Refusing it
here is what keeps the two codecs symmetric - the XML parser drops the
element instead (a value it cannot represent, exactly like a junk one) and a
driver publishing one fails at the call site. The optional metadata
(min/max/step) needs no such rule: it can say "absent", and the
XML parser degrades a non-finite one to None.
Text ¶
Bases: _Element
A single text element (defText / oneText).
Switch ¶
Bases: _Element
A single switch element (defSwitch / oneSwitch).
Light ¶
Bases: _Element
A single light element (defLight / oneLight). Read-only status.
BLOB ¶
Bases: _Element
A single BLOB element (defBLOB / oneBLOB).
data holds the decoded binary payload; the base64/size framing lives in
the codec, not the model.
In JSON the payload is standard base64 (RFC 4648 section 4, the
+// alphabet); validation accepts the URL-safe alphabet as well.
NumberVector ¶
Bases: _Vector
A vector of numeric elements (defNumberVector / setNumberVector).
TextVector ¶
Bases: _Vector
A vector of text elements (defTextVector / setTextVector).
SwitchVector ¶
Bases: _Vector
A vector of switch elements with a selection rule.
selected ¶
selected() -> str | None
Return the name of the first element that is On, or None.
The idiomatic way to read a OneOfMany/AtMostOne client write:
such a write names the newly selected member (often only that
member), so the question is "which element is On in this request" -
never "what is element X", which raises when X was not sent.
Returns:
| Name | Type | Description |
|---|---|---|
name |
str or None
|
The first On element's name, or |
Source code in src/indikit/protocol/models.py
414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 | |
LightVector ¶
Bases: _Vector
A vector of read-only light elements (no perm; lights are always RO).
BLOBVector ¶
Bases: _Vector
A vector of BLOB elements (binary payloads).
GetProperties ¶
Bases: _Model
Client -> device/server request to enumerate properties.
DelProperty ¶
Bases: _Model
Notification that a property (or a whole device) has gone away.
Message ¶
Bases: _Model
A free-form log/notification message.
EnableBLOB ¶
Bases: _Model
Client -> server request controlling BLOB delivery.
indiserver withholds BLOBs from a client until it sends this; the policy
applies to one device (and optionally one property when name is set).
DefVector ¶
Bases: _Model
A property definition (device -> client).
SetVector ¶
Bases: _Model
A value update to an already-defined property (device -> client).
Attributes:
| Name | Type | Description |
|---|---|---|
state_present |
bool
|
Whether the wire message actually carried a |
NewVector ¶
Bases: _Model
A client's request to change a property's value (client -> device).
slugify ¶
slugify(label: str) -> str
Return the conventional INDI element name for a display label.
INDI names are machine identifiers and labels are display text, so the two
differ by exactly this transformation in most drivers: "Domeslit State"
becomes "domeslit_state". The default key for :meth:_Element.from_labels.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
label
|
str
|
The human-readable label. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
name |
str
|
The label lowercased with runs of whitespace collapsed to underscores. |
Source code in src/indikit/protocol/models.py
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | |
as_utc ¶
as_utc(value: datetime) -> dt.datetime
Return a datetime as aware UTC, reading a naive one as UTC.
A bare INDI timestamp carries no offset and means UTC, so that is what a naive datetime is taken to be. Guessing a local offset for an unknown peer would invent information: the same string would mean a different instant depending on where the reader happens to be running.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
datetime
|
The datetime to normalise. Naive values are labelled UTC; aware values in another zone are converted. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
value |
datetime
|
The same instant, expressed in UTC with |
Source code in src/indikit/protocol/models.py
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 | |
indi_now ¶
indi_now() -> dt.datetime
Return the current time the way INDI stamps one: aware UTC, whole seconds.
Truncated to the second because that is the resolution the XML format has: keeping microseconds would let one emission disagree with itself, the JSON carrying a precision the XML for the same message had already dropped.
Returns:
| Name | Type | Description |
|---|---|---|
now |
datetime
|
The current UTC time with a zero microsecond field. |
Source code in src/indikit/protocol/models.py
88 89 90 91 92 93 94 95 96 97 98 99 100 | |
Numbers¶
indikit.protocol.numbers ¶
INDI number text: the printf format and the sexagesimal forms.
The two halves of libindi's fs_sexa / f_scansexa pair, as pure functions
over a value and a format string. They are a protocol concern rather than an
XML one - the same rendering decides what a browser sees through the JSON codec,
and a driver's "on_change" policy compares numbers in exactly this
representation, because "changed" means changed as far as a client can tell.
parse_number ¶
parse_number(text: str) -> float
Parse a number that may be decimal or sexagesimal.
Accepts plain decimals as well as dd:mm:ss (or space-separated)
sexagesimal forms used for RA/Dec, as libindi's f_scansexa does.
Deliberately a superset of libindi on the set path: libindi reads a
oneNumber with std::stod there and only uses f_scansexa on the
def path, so it reads "10:30:00" in a setNumberVector as
10.0. We read sexagesimal in both, which cannot silently truncate a
coordinate. Do not "fix" this to match: matching would mean reading 10:30
as 10, which is a data-corruption bug wearing a compatibility badge.
Strict on purpose, where the XML codec around it is lenient: value is
not nullable on the model, so there is no way to say "absent". Raising here
lets the stream parser drop the whole element rather than publish a reading
a mount would act on.
That covers the non-finite values too. float reads "nan" and
"inf" happily, but neither survives the round trip - JSON cannot write
them, and an integer format cannot render them - so they are refused
here as well as on the model.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
text
|
str
|
The raw element text. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
value |
float
|
The parsed value; |
Raises:
| Type | Description |
|---|---|
ProtocolError
|
Raised (as a ValueError, which the stream parser drops on) if the text is neither a decimal nor a sexagesimal number, or if it names a non-finite value. |
Source code in src/indikit/protocol/numbers.py
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | |
format_number ¶
format_number(value: float, fmt: str) -> str
Format a number per an INDI printf-style format.
Handles ordinary printf conversions as well as the %m sexagesimal form
(e.g. %9.6m), field-width padded like libindi's fs_sexa.
Half-way values round away from zero, which is what fs_sexa does
(indicom.c:165 casts a * fracbase + 0.5 to an integer). Python's
built-in round is half-to-even, so it disagrees on exactly the values
that land on a tick boundary: at %10.6m it renders 0.03125 as
0:01:52 where libindi says 0:01:53. One arcsecond, only on exact
halves, in the format mounts use for RA and Dec.
One divergence from fs_sexa is kept on purpose: libindi passes a
negative width to %*s when w - f < 3 (indicom.c:171), which
left-justifies instead. No real driver declares such a format, and matching
the quirk would only preserve it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
float
|
The number to format. |
required |
fmt
|
str
|
The INDI |
required |
Returns:
| Name | Type | Description |
|---|---|---|
text |
str
|
The formatted value. |
Source code in src/indikit/protocol/numbers.py
105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 | |
Compression¶
indikit.protocol.compression ¶
The INDI .z transport encoding: inflate a BLOB payload on receipt.
The 1.7 whitepaper defines a BLOB's format as a chain of suffixes and says a
trailing .z means the payload is zlib-compressed - .fits.z is a FITS file
that has been deflated for the wire - and that clients are encouraged to support
it. libindi does exactly that in BaseDevicePrivate::setBLOB: every client
built on it inflates the payload, strips the .z and hands the application
.fits. A consumer written against KStars has never seen a .z and will not
look for one, so delivering deflated bytes beside a size describing data we
did not deliver is a correctness bug, not a missing feature.
This lives beside :mod:indikit.protocol.numbers rather than inside
:mod:indikit.protocol.xml for the same reason that one does: it is not an
XML concern. A BLOB reaches a browser as JSON off the same models, and a
browser must no more see a .z than a Python application does, so the rule
belongs where both codecs can call it. Doing it in :mod:indikit.client.store
instead - where libindi happens to put it - would cover the client and leave the
driver's inbound newBLOBVector and the whole JSON path uncovered.
Compressing is receive-only in the sense that matters: nothing here ever
deflates anything. That stays the driver author's decision, as it is in libindi
(an opt-in CCD_COMPRESSION switch, defaulting to off). A driver may set
format=".fits.z" with deflated bytes and an explicit uncompressed size,
and both codecs serialise that faithfully; what neither will do is guess the
size it cannot know, which is :func:require_declared_size.
zlib_encoded ¶
zlib_encoded(fmt: str | None) -> bool
Whether a BLOB format marks its payload as zlib-compressed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
fmt
|
str or None
|
The BLOB's |
required |
Returns:
| Name | Type | Description |
|---|---|---|
encoded |
bool
|
|
Source code in src/indikit/protocol/compression.py
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | |
require_declared_size ¶
require_declared_size(el: BLOB) -> None
Refuse to emit a compressed payload whose uncompressed length is unstated.
INDI's size is the decoded and uncompressed length, so len(data)
is the right default only for a payload that is neither encoded nor
compressed. A .z format says the bytes are deflated, and the number the
attribute is defined as could only be learned by inflating them - work the
driver did not ask for on a path where it already knows the answer. So it
has to say, and a model that does not is refused rather than described with
the wrong number.
Both codecs call this, and that is the point: they serialise the same models, so a frame one of them will emit and the other will not is drift between two descriptions of one contract. Keeping the rule here rather than in either codec is what stops it being written twice and diverging once. Refusing is loud and contained - the driver runtime's writer reports a message it cannot serialise and drops it, rather than taking the driver down.
.fz is untouched, as everywhere else in this module: it is a container
format whose size the driver states for reasons of its own.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
el
|
BLOB
|
The element about to be serialised. |
required |
Raises:
| Type | Description |
|---|---|
ProtocolError
|
Raised if the element carries a payload and a |
Source code in src/indikit/protocol/compression.py
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 | |
inflate_blob ¶
inflate_blob(el: BLOB) -> None
Inflate a received .z payload in place, mirroring libindi exactly.
Three things change together, and an application must never see any of them
half-applied: the payload becomes the inflated bytes, format loses its
.z, and size becomes the inflated length. Anything else that is
already what a caller wants - an uncompressed payload, a .fz frame, a
def carrying no bytes at all - is left exactly as it arrived.
A def-shaped BLOB is untouched even when its format says .z:
there is no payload to inflate, and renaming a format on the strength of
bytes that never arrived would describe a frame nobody sent. The set
that carries the payload brings the corrected format with it, and a client's
merge takes the format from the set.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
el
|
BLOB
|
The freshly parsed element, mutated in place. |
required |
Raises:
| Type | Description |
|---|---|
ProtocolError
|
Raised if the payload will not inflate. Also a ValueError, so the XML
stream parser's documented drop-and-count path applies and a corrupt
frame costs the message carrying it rather than the connection. The
compressed bytes are never delivered instead: a caller that asked for a
|
Source code in src/indikit/protocol/compression.py
128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 | |
XML codec¶
indikit.protocol.xml ¶
INDI 1.7 XML codec: models <-> canonical INDI XML.
Two directions:
- :func:
to_xmlserializes a model (DefVector/SetVector/NewVectoror a bare message) to the exactdef*/set*/new*XML thatindiserverand C++ INDI clients expect. - :class:
XMLStreamParserconsumes the raw byte stream from a socket or stdio pipe and yields fully-formed :data:~indikit.protocol.models.IndiMessageobjects as complete top-level elements arrive, reassembling messages across arbitrary chunk boundaries (BLOB payloads included).
Parsing is lenient by policy. The peer at the other end is somebody else's C
driver, and one element it malformed must not take down a session that is
otherwise working: an unparseable optional attribute degrades to absent, an
element whose value cannot be parsed at all is dropped and counted, and an
unmatched close tag that would end the document reopens it. What leniency never
does is invent a value - see :func:~indikit.protocol.numbers.parse_number
for a leaf and :func:_required for the device/name a message is
nothing without.
Number values honour the INDI printf-style format, including the %m
sexagesimal form used for RA/Dec, so values round-trip faithfully with libindi.
That rendering is not an XML concern and lives in
:mod:indikit.protocol.numbers; this module only calls it.
XMLStreamParser ¶
XMLStreamParser()
Incremental parser for the unbounded INDI element stream.
The INDI wire is a sequence of sibling top-level elements with no enclosing document root, so we feed a synthetic root and emit each depth-1 element as it completes, clearing consumed nodes to keep memory flat.
Nothing a peer can send makes this raise. A stream parser that throws takes the whole session with it - on a driver, the raise escapes the runtime's per-message isolation because it happens while iterating this generator, and on a client it kills the reconnect loop. So malformed input is absorbed and counted instead, and the counters are how a caller finds out.
Attributes:
| Name | Type | Description |
|---|---|---|
dropped |
int
|
Top-level elements discarded because a value would not parse and the model had no way to say "absent". An interop signal: it means somebody's codec is emitting something this one will not read. |
resets |
int
|
Times the synthetic document had to be reopened - by :meth: |
bytes_since_last_message |
int
|
Bytes fed since a top-level element last completed. Compared against
:data: |
Both counters describe *the peer on this stream*, not the lifetime of one |
|
|
lxml object |
:meth:`resync` rebuilds the parser underneath them and leaves
|
|
them running, so a reader that rebuilds behind the caller's back does not |
|
|
quietly zero the history it is about to want. |
|
Start the pull parser and open a synthetic enclosing root element.
Source code in src/indikit/protocol/xml.py
757 758 759 760 761 762 | |
stalled
property
¶
stalled: bool
Whether bytes keep arriving but no message has come out for far too long.
The one failure this class cannot see from the inside: lxml can be left
in a state that emits no event at all - a root close arriving while a
start tag is half-parsed does it, with nothing in error_log - and
from then on every message is swallowed in silence. A reader loop holds
both halves of the evidence, so it asks this and then either calls
:meth:resync (a driver, which has only the one stdin) or drops the
connection (a client, which can just reconnect).
It also catches the stream that never produces anything in the first place - nothing but unmatched close tags, say - because reopening the document is not progress and leaves this counter running.
resync ¶
resync() -> None
Rebuild the parser after a reader has judged the stream :attr:stalled.
The remedy for a parser that has gone mute: the lxml object is thrown
away and a fresh document opened, so the stream picks up again at the
next well-formed element. :attr:dropped and :attr:resets survive,
because they describe the peer on the other end and not the object being
replaced - and a peer's malformed-input history is never more
interesting than at the moment its stream stops saying anything.
The stall budget does start again here, unlike after :meth:_reset: the
reader has just applied the remedy, and leaving the counter above the
threshold would report a stall on every later chunk. The reason for the
rebuild belongs to the caller, which has the device name or the peer
address to log it against, so this method stays quiet.
Source code in src/indikit/protocol/xml.py
793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 | |
feed ¶
feed(data: bytes | str) -> Iterator[IndiMessage]
Feed the next chunk of bytes and yield any completed messages.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
bytes or str
|
The next bytes or string from the stream. |
required |
Yields:
| Name | Type | Description |
|---|---|---|
message |
IndiMessage
|
Each top-level message that completed within this chunk. |
Source code in src/indikit/protocol/xml.py
813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 | |
to_xml ¶
to_xml(msg: IndiMessage, *, pretty: bool = False) -> bytes
Serialise an INDI message model to canonical INDI XML bytes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
msg
|
IndiMessage
|
The message model to serialise. |
required |
pretty
|
bool
|
Whether to pretty-print the output. |
False
|
Returns:
| Name | Type | Description |
|---|---|---|
xml |
bytes
|
The encoded XML. |
Raises:
| Type | Description |
|---|---|
ProtocolError
|
Raised if a BLOB declares a compressed |
Source code in src/indikit/protocol/xml.py
324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 | |
message_from_xml ¶
message_from_xml(node: _Element) -> IndiMessage | None
Convert a single top-level INDI element node to a message model.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
node
|
_Element
|
A top-level INDI element node. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
message |
IndiMessage or None
|
The parsed message, or |
Raises:
| Type | Description |
|---|---|
ValueError
|
Raised if a value the model cannot represent as absent is malformed -
a number's text, a BLOB's base64, one of the state/permission/rule
tokens, or a missing |
Source code in src/indikit/protocol/xml.py
630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 | |
parse_indi ¶
parse_indi(data: bytes | str) -> list[IndiMessage]
Parse a complete chunk of INDI XML into message models.
Convenience wrapper over :class:XMLStreamParser for a self-contained chunk
that holds one or more complete top-level elements, and it inherits that
class's leniency: a malformed element is dropped, not raised, so a shorter
list than expected - not an exception - is how bad input shows up here.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
bytes or str
|
The XML bytes or string to parse. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
messages |
list of IndiMessage
|
Every message found in the chunk, in order. |
Source code in src/indikit/protocol/xml.py
696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 | |
JSON codec¶
indikit.protocol.json ¶
INDI JSON codec: models <-> typed JSON for browser clients.
The INDI wire toward indiserver is XML (see :mod:indikit.protocol.xml);
toward browsers it is JSON. Both directions serialise the same Pydantic models,
so the JSON contract is just the models dumped to JSON - one source of truth. The
frontend's TypeScript types are not generated from it: INDI 1.7 is frozen, so
web/packages/client/src/types.ts is a hand-authored mirror of these models and
has to be updated in step with them.
:func:to_json and :func:from_json mirror to_xml / parse_indi over a
single :class:pydantic.TypeAdapter for
:data:~indikit.protocol.models.IndiMessage. That union is discriminated on
tag, the way the element and vector unions are discriminated on kind,
which is what makes it closed: a payload whose tag is missing or unknown is
refused outright rather than matched against every member in turn. Left
undiscriminated it was not closed at all - :class:GetProperties defaults every
field and the base model ignores extras, so {} and any other unrecognised
object validated as a getProperties. BLOB payloads travel as base64
(configured on the base model).
The only thing either function does beyond (de)serialising is the .z rule
(see :mod:indikit.protocol.compression), and it is applied here for the same
reason the XML codec applies it: these two read and write the same models, so a
payload one of them inflates and the other does not, or a frame one will emit and
the other refuses, is drift between two descriptions of one contract. On the way
in a zlib-compressed BLOB payload is inflated and its format loses the
suffix, so the browser at the end of this contract - which has no zlib of its own
- never meets one. On the way out a .z format with no explicit size is
refused, exactly as to_xml refuses it.
Everything :func:to_json writes, :func:from_json reads back. JSON has no
literal for NaN or the infinities, so a non-finite Number.value would be
serialised as null and then rejected on the way back in - the payload would
be unreadable by its own codec. The models refuse a non-finite value
instead (see :class:~indikit.protocol.models.Number), which puts the
failure at the point the value enters rather than on the far side of a network
hop, and the XML parser drops such an element for the same reason.
to_json ¶
to_json(msg: IndiMessage) -> str
Serialise an INDI message model to a JSON string.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
msg
|
IndiMessage
|
The message model to serialise. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
text |
str
|
The message as JSON (a |
Raises:
| Type | Description |
|---|---|
ProtocolError
|
Raised (also a |
Source code in src/indikit/protocol/json.py
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 | |
from_json ¶
from_json(data: str | bytes) -> IndiMessage
Parse a JSON string into the matching INDI message model.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
str or bytes
|
A single JSON object with a |
required |
Returns:
| Name | Type | Description |
|---|---|---|
message |
IndiMessage
|
The parsed, fully typed message model. |
Raises:
| Type | Description |
|---|---|
ValidationError
|
Raised (as a |
ProtocolError
|
Raised (also a |
Source code in src/indikit/protocol/json.py
108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 | |