Codec
Documents in, FeatureCollections out - and back.
A codec turns a whole document into a FeatureCollection and back, and one feature into
that encoding's representation of it (encode_feature / decode_feature). Two concerns
that used to be one class are separate:
SourceandTargetsay where a document's bytes are - on disk, behind a URL, in memory - and nothing about what is in them.- A
Codecsays what the bytes mean, and is stateless.
read and write put the two together, which is all most callers need:
from xmas_core.codec import read, write
collection = read("plan.gml") # encoding from the content
collection = read(wfs_url, allow_remote=True) # ...which is why a URL works
write(collection, "out.json") # encoding from the target's name
write(collection, sys.stdout.buffer, format="gml") # ...or said outright
Where the encoding is known up front, the codec module is reached directly:
Reading identifies the encoding from the document's content - an XML document by its root element, JSON-FG by its first byte - and falls back to the file extension. Writing goes by the target's name only, because whatever is at the target is about to be replaced and so says nothing about what is being written.
Adding an encoding is a module here, its name and file extensions in datasource -
Format, _SUFFIX_FORMATS, and a _sniff branch when the document opens with neither
< nor { - and one entry in CODECS. There is deliberately no registration call
that would do all four at once: Format is a Literal, which is what checks a caller's
format= and CODECS[encoding] statically, and a runtime registry would have to give
that up. Nothing derives the list from anything else either - datasource sniffs to a
Format and knows no codec, and each codec knows only its own - so the two halves are
held together from outside: the type checker refuses a CODECS key that is not a
Format, and test_every_registered_codec_satisfies_the_interface refuses a Format with no
codec behind it. A codec is stateless, so it is its module - gml.decode(source),
nothing to construct.
A database is not a document: it holds no stream of bytes, and
DBRepository addresses one through SQLAlchemy.
A connection URL handed here is refused with that route named.
CODECS
module-attribute
The encoding each Format is read and written with.
The modules themselves: a codec holds no state, so there is nothing to construct. This is
the only place that knows the list; datasource identifies a document as a Format
without knowing what reads it.
read
read(source: Any, *, allow_remote: bool | None = None, **options: Any) -> FeatureCollection
Reads a whole document, whatever encoding it turns out to be.
The encoding is identified from the content, so a document served under no usable name
- a WFS GetFeature response, say - is still read correctly.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
Any
|
A file path as a string or |
required |
allow_remote
|
bool | None
|
Whether an |
None
|
**options
|
Any
|
Read options for the encoding; see
|
{}
|
Raises:
| Type | Description |
|---|---|
DatasourceError
|
The document could not be identified. |
UnsupportedDatasourceError
|
The source names a location this package refuses to open, or is remote while remote access is disabled. |
ForbiddenDoctypeError
|
The document is XML and declares a DTD. |
UnsupportedRootElementError
|
The document is XML, but opens with an element this package does not read. |
RemoteFetchError
|
A remote source could not be retrieved. |
Returns:
| Type | Description |
|---|---|
FeatureCollection
|
The collection the document holds. |
Source code in xmas_core/codec/__init__.py
write
write(collection: FeatureCollection, target: Any, *, format: Format | None = None, **options: Any) -> None
Writes a collection as a document, replacing whatever is at target.
The encoding comes from the target's name. Where the name does not settle it - a
file-like object, or an extension this package does not know - pass format.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
collection
|
FeatureCollection
|
The collection to write. |
required |
target
|
Any
|
A file path as a string or |
required |
format
|
Format | None
|
The encoding to write, when the target's name does not give it. |
None
|
**options
|
Any
|
Write options for the encoding; see
|
{}
|
Raises:
| Type | Description |
|---|---|
DatasourceError
|
The encoding could not be determined from the target's name. |
UnsupportedDatasourceError
|
The target names a location this package refuses to write to. |
Source code in xmas_core/codec/__init__.py
Codec
Bases: Protocol
Decodes a document into a collection, and encodes one back out.
Everything either direction needs arrives as an argument, which is why an implementation can be a module: there is no state for one to hold.
decode
decode(source: Source, **options: Any) -> FeatureCollection
Reads the whole document at source.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
Source
|
The document to read. |
required |
**options
|
Any
|
Read options this encoding understands; the rest are ignored. |
{}
|
Returns:
| Type | Description |
|---|---|
FeatureCollection
|
The collection the document holds. |
Source code in xmas_core/codec/interface.py
encode
encode(collection: FeatureCollection, target: Target, **options: Any) -> None
Writes collection to target, replacing whatever is there.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
collection
|
FeatureCollection
|
The collection to write. |
required |
target
|
Target
|
Where to write it. |
required |
**options
|
Any
|
Write options this encoding understands; the rest are ignored. |
{}
|
Source code in xmas_core/codec/interface.py
datasource
Where a document's bytes are, and how to get at them.
A codec decodes and encodes; it has no business knowing whether the bytes sit on disk,
behind a URL or already in memory. That is what Source and Target hold, which is why
the codecs in this package take one of them and keep no state of their own.
from xmas_core.codec.datasource import Source, Target
Source("plan.gml").read_bytes() # opened here, not before
Source(wfs_url, allow_remote=True).format # "gml", from what it served
Target("out.gml").write_bytes(payload)
Reading and writing are not symmetric, so they are two classes rather than one with a flag:
- A source is identified by its content - an XML document by its root element, JSON-FG by its first byte - and only then by its file extension, so a URL serving a document under no usable name is still read correctly. Content sniffing is done here rather than by GDAL, which took 75 s to identify a 68 MB GML.
- A target is identified by its name alone. Whatever is at it is about to be replaced, so it says nothing about what is being written, and reading it would refuse to overwrite a document this package cannot read.
- A target is never fetched, so an
http(s)URL is not aTargetat all and is refused when one is constructed, rather than at the write.
Construction classifies and validates but never reads: a path is opened, and a remote URL
retrieved, only when something first needs the bytes. The remote policy is enforced
eagerly, since consulting it costs nothing. Retrieval happens here rather than inside
lxml, so the parser keeps its network access switched off and the timeout, redirect and
size limits live in one place. Remote access is off unless XMAS_DS_ALLOW_REMOTE is set
or allow_remote is passed; with it on, the URL is retrieved as given, so a caller
forwarding an untrusted source is responsible for restricting where it may point.
A database is not a document: it holds no stream of bytes, and
DBRepository addresses one through
SQLAlchemy. A connection URL handed here is refused with that route named.
Format
module-attribute
The document encodings this package reads and writes.
Source
Bases: _Datasource
A document to read: where its bytes are, and which encoding they are in.
Built once and handed to the codec that reads it, so a remote document is retrieved once and a one-shot stream drained once, however many times the encoding and the root element are consulted on the way.
Usage example
Attributes:
| Name | Type | Description |
|---|---|---|
raw |
The source exactly as it was handed over. |
|
kind |
Kind
|
Where its bytes are. |
allow_remote |
Whether retrieval is permitted; |
Classifies and validates a source, without reading it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
Any
|
A file path as a string or |
required |
allow_remote
|
bool | None
|
Whether an |
None
|
Raises:
| Type | Description |
|---|---|
UnsupportedDatasourceError
|
The source names a location this package refuses to open, or is remote while remote access is disabled. |
Source code in xmas_core/codec/datasource.py
buffer
cached
property
The source's bytes, when they are in memory rather than on disk.
Retrieves a remote source on first access. A path is left for the codec to open,
so a large file is never read into memory here. A stream that cannot be rewound is
drained into a BytesIO once, so that identifying the encoding does not consume
it and leave the codec with nothing.
Raises:
| Type | Description |
|---|---|
RemoteFetchError
|
A remote source could not be retrieved. |
format
property
format: Format | None
The encoding this document is written in, or None if it is unidentified.
Read from the content, falling back to the file extension. Sniffing is what retrieves a remote document, so the bytes it read are the ones the codec goes on to parse.
Raises:
| Type | Description |
|---|---|
ForbiddenDoctypeError
|
The document is XML and declares a DTD. |
UnsupportedRootElementError
|
The document is XML, but opens with an element this package does not read. |
RemoteFetchError
|
A remote source could not be retrieved. |
root_info
property
The root element of an XML document, read without parsing the rest of it.
Identifying the encoding already read it, so a GML document is preflighted once however it was reached.
Raises:
| Type | Description |
|---|---|
ForbiddenDoctypeError
|
The document declares a DTD. |
UnsupportedRootElementError
|
The document opens with an element this package does not read. |
XMLParseError
|
The document does not hold well-formed XML. |
read_bytes
Returns the whole document, retrieving a remote one on the way.
A path is opened and closed here; a buffer is rewound and put back where it was found, so a caller's stream comes back as it was handed over.
Raises:
| Type | Description |
|---|---|
RemoteFetchError
|
A remote source could not be retrieved. |
OSError
|
The path could not be opened. |
Returns:
| Type | Description |
|---|---|
bytes
|
The document's bytes. |
Source code in xmas_core/codec/datasource.py
Target
Bases: _Datasource
A location to write a document to.
A target is never fetched, so an http(s) URL is refused here rather than at the
write, and there is nothing to read: whatever is at the target now is about to be
replaced.
Attributes:
| Name | Type | Description |
|---|---|---|
raw |
The target exactly as it was handed over. |
|
kind |
Kind
|
Where its bytes go. |
Classifies and validates a target, without writing to it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
target
|
Any
|
A file path as a string or |
required |
Raises:
| Type | Description |
|---|---|
UnsupportedDatasourceError
|
The target names a location this package refuses to write to. |
Source code in xmas_core/codec/datasource.py
format
property
format: Format | None
The encoding this target's name asks for, or None when it does not say.
The counterpart of Source.format,
and deliberately not the same rule: there is nothing at a target worth reading,
because it is about to be replaced.
sibling
sibling(name: str) -> Target
A target next to this one, named after it.
What a codec that writes a family of documents rather than one needs: JSON-FG writes one document per feature type, and writing them all here would replace the target once per type and leave only the last. The sibling keeps the target's extension, so it is the same encoding by construction.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Appended to the target's stem, after an underscore. |
required |
Raises:
| Type | Description |
|---|---|
DatasourceError
|
The target is not a file path, so it names no family. |
Returns:
| Type | Description |
|---|---|
Target
|
A target over the sibling path. |
Source code in xmas_core/codec/datasource.py
write_bytes
Writes data to the target, encoding for a text buffer.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
bytes
|
The serialized document. |
required |
Raises:
| Type | Description |
|---|---|
OSError
|
The path could not be written. |
Returns:
| Type | Description |
|---|---|
int
|
The number of bytes, or characters for a text buffer, written. |
Source code in xmas_core/codec/datasource.py
fetch_remote
fetch_remote(url: str, *, allow_remote: bool | None = None, timeout: float | None = None, max_bytes: int | None = None) -> BytesIO
Retrieves a remote document into a buffer.
The buffer can be handed straight to a Source. Retrieval happens here, rather than
inside lxml, so that the parser needs no network access of its own.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
url
|
str
|
An |
required |
allow_remote
|
bool | None
|
Whether remote retrieval is permitted; defaults to the
|
None
|
timeout
|
float | None
|
Seconds to wait; defaults to the |
None
|
max_bytes
|
int | None
|
Largest response body accepted; defaults to the
|
None
|
Raises:
| Type | Description |
|---|---|
UnsupportedDatasourceError
|
Remote retrieval is disabled, or the URL does not name
an |
RemoteFetchError
|
The request failed, or the response exceeded |
Returns:
| Type | Description |
|---|---|
BytesIO
|
The response body, positioned at the start. |
Source code in xmas_core/codec/datasource.py
gml
The GML codec: a whole XPlanGML document in, a FeatureCollection out, and back.
encode_feature and decode_feature turn one feature into one gml:featureMember
and back. Both directions are driven by the appschema metadata - a property's stereotype,
typename and uom - rather than by the shape of the data or of the document. A data type is
encoded inline inside its owner, so the recursion into it only needs its class metadata.
Everything else here is what a document adds on top: which element it opens with, which
namespaces it declares, where its SRS is written, and the document-wide gml:id
bookkeeping that no single feature can do on its own.
Reading a document is two passes over its feature elements, because every id has to be
settled before the first feature is read: a gml:id that is not a UUID, or that repeats
a UUID another feature already claimed, is replaced, and every xlink:href naming it has
to be repointed before anything is validated.
The codec is this module: it holds no state, so there is nothing to construct.
from xmas_core.codec import gml
from xmas_core.codec.datasource import Source, Target
collection = gml.decode(Source("plan.gml"))
gml.encode(collection, Target("out.gml"))
decode
decode(source: Source, *, always_generate_ids: bool = False, context: dict[str, Any] | None = None, **_: Any) -> FeatureCollection
Reads a whole GML document.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
Source
|
The document to read. |
required |
always_generate_ids
|
bool
|
Generate new feature ids even where the |
False
|
context
|
dict[str, Any] | None
|
Pydantic validation context for the collection. Setting
|
None
|
**_
|
Any
|
Options for another encoding, ignored here. |
{}
|
Raises:
| Type | Description |
|---|---|
AppschemaNotFoundError
|
No supported application schema was declared. |
SRSNotFoundError
|
The document declares no SRS. |
DuplicateGmlIdError
|
Two features share a |
Returns:
| Type | Description |
|---|---|
FeatureCollection
|
The collection the document holds. |
Source code in xmas_core/codec/gml.py
decode_feature
decode_feature(cls: type[F], element: _Element, srs: SRS) -> F
Decodes one feature's GML element as cls.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cls
|
type[F]
|
The feature type the element names. |
required |
element
|
_Element
|
The feature's element. |
required |
srs
|
SRS
|
The document's SRS, for a geometry that names none of its own. |
required |
Returns:
| Type | Description |
|---|---|
F
|
The validated feature. |
Source code in xmas_core/codec/gml.py
encode
encode(collection: FeatureCollection, target: Target, *, feature_srs: bool = True, **_: Any) -> None
Writes a collection as a GML document.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
collection
|
FeatureCollection
|
The collection to write. |
required |
target
|
Target
|
Where to write it. |
required |
feature_srs
|
bool
|
Write |
True
|
**_
|
Any
|
Options for another encoding, ignored here. |
{}
|
Raises:
| Type | Description |
|---|---|
AppschemaNotFoundError
|
The collection's application schema has no GML encoding in this package. |
Source code in xmas_core/codec/gml.py
319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 | |
encode_feature
encode_feature(feature: FeatureType, *, feature_srs: bool = True) -> _Element
Encodes one feature as its GML element, the content of a gml:featureMember.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
feature
|
FeatureType
|
The feature to encode. |
required |
feature_srs
|
bool
|
Write |
True
|
Returns:
| Type | Description |
|---|---|
_Element
|
The feature's element. |
Source code in xmas_core/codec/gml.py
jsonfg
The JSON-FG codec: a whole JSON-FG document in, a FeatureCollection out, and back.
encode_feature and decode_feature turn one feature into one JSON-FG feature object
and back. Everything else here is what a document adds: the
collection object that wraps the features, the describedby link the appschema is read
from, the conformance classes it declares, and the document-wide id bookkeeping.
JSON-FG 1.0 lets a document declare its feature type once on the collection (req 2 H)
instead of on every feature, which is what writing one document per feature type produces.
Both shapes are read, and either can be written: single_collection=False fans the
collection out into one document per feature type, written beside the target.
The codec is this module: it holds no state, so there is nothing to construct.
from xmas_core.codec import jsonfg
from xmas_core.codec.datasource import Source, Target
collection = jsonfg.decode(Source("plan.json"))
jsonfg.encode(collection, Target("out.json"))
decode
decode(source: Source, *, context: dict[str, Any] | None = None, **_: Any) -> FeatureCollection
Reads a whole JSON-FG document.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
Source
|
The document to read. |
required |
context
|
dict[str, Any] | None
|
Pydantic validation context for the collection. Setting
|
None
|
**_
|
Any
|
Options for another encoding, ignored here. |
{}
|
Raises:
| Type | Description |
|---|---|
JsonFGParseError
|
The document is not readable JSON-FG. |
AppschemaNotFoundError
|
No supported application schema was declared. |
Returns:
| Type | Description |
|---|---|
FeatureCollection
|
The collection the document holds. |
Source code in xmas_core/codec/jsonfg.py
decode_feature
decode_feature(cls: type[F], feature: dict[str, Any], srs: SRS) -> F
Decodes one JSON-FG feature object as cls.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cls
|
type[F]
|
The feature type the object holds. |
required |
feature
|
dict[str, Any]
|
The feature object. |
required |
srs
|
SRS
|
The document's SRS, for a |
required |
Returns:
| Type | Description |
|---|---|
F
|
The validated feature. |
Source code in xmas_core/codec/jsonfg.py
encode
encode(collection: FeatureCollection, target: Target, *, single_collection: bool = True, write_geometry: bool = True, write_bbox: bool = True, **_: Any) -> None
Writes a collection as one JSON-FG document, or one per feature type.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
collection
|
FeatureCollection
|
The collection to write. |
required |
target
|
Target
|
Where to write it. With |
required |
single_collection
|
bool
|
Write everything as one document. |
True
|
write_geometry
|
bool
|
See |
True
|
write_bbox
|
bool
|
See |
True
|
**_
|
Any
|
Options for another encoding, ignored here. |
{}
|
Raises:
| Type | Description |
|---|---|
DatasourceError
|
A fan-out was asked for and the target is not a file path. |
Source code in xmas_core/codec/jsonfg.py
encode_feature
encode_feature(feature: FeatureType, *, write_featuretype: bool = True, write_geometry: bool = True, write_bbox: bool = True) -> dict[str, Any]
Encodes one feature as a JSON-FG feature object.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
feature
|
FeatureType
|
The feature to encode. |
required |
write_featuretype
|
bool
|
Write |
True
|
write_geometry
|
bool
|
Write the WGS 84 |
True
|
write_bbox
|
bool
|
Write |
True
|
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
The feature object. |