s-layer-semantic-formats
The shared definition of the S-LAYER semantic format vocabulary: the s_layer block that
says what a metric, fact or dimension means (a currency in USD, a fraction shown as a
percentage, a duration in seconds, a monthly date), written once in dbt config.meta.
- dbt-s-layer validates blocks and writes them into each warehouse.
- semolina reads them back.
This package holds what both sides must agree on:
- typed pydantic v2 models for the block, with
validate_block()for a member type; - the normal form that defines equality (
normalize()); - the compact
S_LAYER_SPECtag payload used by Snowflake and DuckDB semantic views, and its inverse; - the JSON Schema, generated from the models and shipped as
s_layer_semantic_formats/s_layer.schema.json.
It contains no warehouse code and no dbt code.
The block
metrics:
- name: revenue
config:
meta:
s_layer:
value: {kind: currency, currency: USD}
display: {decimals: {max: 0}, compact: auto}
synonyms: [sales, turnover]
| Part | Keys |
|---|---|
value |
kind (required) plus the kind's own keys, such as currency or unit |
display |
decimals {min, max}, compact, negative, grouping, date_style |
time |
grain, fiscal_year_start (date and datetime dimensions only) |
aggregation |
additivity, plus by when semi_additive (metrics only) |
label |
only where the platform has no label slot |
synonyms |
only where the platform has no synonyms slot |
x-<vendor> |
extension keys, carried verbatim but not interpreted |
Kinds: number, count, currency, percentage, duration, bytes, quantity, score
(metrics, and some facts) and category, geo, date, datetime, identifier, boolean
(dimensions). KIND_MEMBER_TYPES has the exact table.
Usage
from s_layer_semantic_formats import (
SLayerValidationError,
from_compact_json,
normalize,
spec_tag_value,
validate_block,
)
block = validate_block(
{
"value": {"kind": "currency", "currency": "USD"},
"display": {"decimals": {"max": 0}, "compact": "auto"},
},
"metric",
)
normalize(block)
# {'display': {'compact': 'auto', 'decimals': [0, 0],
# 'grouping': True, 'negative': 'minus'},
# 'value': {'currency': 'USD', 'kind': 'currency'}}
# The S_LAYER_SPEC tag value (raises SpecTooLongError past 256 chars)
spec = spec_tag_value(block)
# '{"display":{"compact":"auto","decimals":[0,0],"grouping":true,"negative":"minus"},"value":{"currency":"USD"}}'
# Reading the tags back gives an equal block
assert from_compact_json("currency", spec) == block
try:
validate_block({"value": {"kind": "currency"}}, "dimension")
except SLayerValidationError as error:
print(error)
# invalid s_layer block (1 problem):
# value.currency: required key is missing
SLayerValidationError.issues lists every problem as an Issue(path, message).
Public API
| Name | What it does |
|---|---|
validate_block(data, member_type) |
Parse a block and check its kind applies to the member type |
parse_block(data) |
Parse a block without the member-type check |
member_type_issues(block, member_type) |
The member-type problems of a parsed block, as a list |
normalize(block) |
The normal form as JSON-ready data |
to_compact_json(block) |
The S_LAYER_SPEC payload |
spec_tag_value(block, limit=256) |
to_compact_json, raising SpecTooLongError when too long |
from_compact_json(kind, spec, *, label, synonyms, ...) |
Rebuild a block from S_LAYER_KIND, S_LAYER_SPEC and label |
implied_additivity(kind) |
"non_additive" for percentage and score, else None |
json_schema(), load_schema(), SCHEMA_PATH |
The generated schema, and the committed copy |
SLayerBlock and the section and value models |
The typed block; block.kind, block.additivity, .extensions |
Blocks compare equal (==, hash) on their normal form, so {max: 2} and
{min: 0, max: 2} are the same value.
Decisions not in the design doc
The design doc is a draft. Where it is silent, this package picks the simplest rule it could find:
- Defaults the normal form fills in are only the ones the doc states: percentage
scale: fraction, bytesunit: B, decimalsmin: 0, and on numeric kindsnegative: minusandgrouping: true;identifiergetsgrouping: false. Every other optional key (compact,date_style,grain,fiscal_year_start,code, numberunit) is left out when absent, so a missingcompactis not equal tocompact: none. - Implied additivity is not written into the block.
percentageandscoreimplynon_additive, exposed byimplied_additivity()andSLayerBlock.additivity, but the normal form andS_LAYER_SPEConly carry anaggregationsomeone wrote ("derive, don't duplicate"). A statedaggregationon those kinds must benon_additive. - Required kind keys:
duration.unit,score.rangeandgeo.levelare required, as their table rows have no?and no default.boolean.labelsneeds bothtrueandfalse; unquoted YAMLtrue:/false:keys (read as booleans) are accepted too. - Allowed values the doc leaves open: bytes units are
B,KB…EBandKiB…EiB. A number's optionalunitis any non-empty string, like quantity's UCUM code. Currency is checked against^[A-Z]{3}$only. A geocodeis any non-empty string. - Score range: two finite numbers with
min < max; whole numbers are written as integers in the normal form, so[0.0, 10]and[0, 10]are equal. - Decimals: integers 0-20,
maxis required whendecimalsis given,min <= max. Both{min, max}and[min, max]are accepted; the normal form is the pair. - Display keys must fit the kind:
decimals,compactandnegativeonly on numeric kinds;groupingon numeric kinds and onidentifier(where onlyfalseis allowed);date_styleonly ondateanddatetime. timeonly ondateanddatetime, and adatecannot take a grain finer than a day (second,minute,hour).aggregationonly on metrics (checked byvalidate_block).byis required exactly whensemi_additive.- Lists (
category.order,aggregation.by,synonyms) are non-empty lists of distinct non-empty strings. Their order is kept, since it can carry meaning. labelis 1-255 characters, which fits both Databricksdisplay_name(255) and the 256-character tag limit. Whether a platform has a native slot (and so whetherlabelorsynonymsbelong in the block at all) is left to dbt-s-layer.- Extension keys are allowed on the block and on
value,display,timeandaggregation, not insidedecimals,labelsorrange. A key is an extension when it starts with lower-casex-and has at least one more character. Values can be any JSON (so no NaN or Infinity) and are kept as they are; only object key order is sorted in the normal form. nullmeans absent for any optional key, and an emptydisplay: {}ortime: {}is the same as leaving it out.- Strict scalar types: no coercion from strings or between bools and numbers
(
grouping: "yes",fiscal_year_start: "4"anddecimals.max: 2.0are errors). S_LAYER_SPECkeeps the block's nesting ({"display": {...}, "value": {...}}) minusvalue.kind,labelandsynonyms; an emptyvalueobject is dropped, so a baredateblock's spec is{}. Non-ASCII text is written as is, and the 256 limit counts characters and is inclusive.from_compact_jsonrejects a spec that containskind,labelorsynonyms, or NaN or Infinity. It treats an empty label or synonyms list as none, and rejects a bare string of synonyms.- Equality compares the canonical JSON text of the normal form, so extension values
1,1.0andtrueare different. - The JSON Schema checks shape only. Which kinds apply to which member type, and the
rules between sections (items 2, 7, 8, 9), are enforced by the Python models and
validate_block. So are a few rules within one key that the schema does not state:2.0where an integer is required (item 14),min <= maxfor decimals andmin < maxfor a score range (items 5, 6),bybeing present exactly whensemi_additive(item 9), and finite extension values (item 12). A block that passes the schema can still failvalidate_block. The schema acceptsnullfor optional keys, matching item 13.
Development
uv sync --dev
prek install # git hooks
uv run pytest # tests, including hypothesis property tests
prek run --all-files # ruff, basedpyright, uv lock and schema checks
just schema # regenerate s_layer.schema.json after changing the models
Without just, regenerate the schema with uv run python scripts/regen_schema.py
(--check only compares). See DEVELOP.md for the full guide.
License
MIT
Metadata
Release files for s-layer-semantic-formats 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| s_layer_semantic_formats-0.1.0.tar.gz | 20.3 kB | With provenance | Details for s_layer_semantic_formats-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| s_layer_semantic_formats-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for s_layer_semantic_formats-0.1.0-py3-none-any.whl |
Total release size: 43.4 kB
Release files / s_layer_semantic_formats-0.1.0.tar.gz
| Download URL | s_layer_semantic_formats-0.1.0.tar.gz |
|---|---|
| Size | 20.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
1c4acd38927bde2d9e069e58517bf2c4dd6d4412645e9c17b3cfd06a729cafb0
|
|
BLAKE2b-256 checksum How to use checksums |
56c6dabe499e1702fb358f0fcf349511fd50f21235df8251b9a9fca68f04c4a5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 26, 2026.
Transparency logRelease files / s_layer_semantic_formats-0.1.0-py3-none-any.whl
| Download URL | s_layer_semantic_formats-0.1.0-py3-none-any.whl |
|---|---|
| Size | 23.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
976beb9d06f1924bd58d10969ab5add72f2906e08b2049c1bf3191f3858b8e1f
|
|
BLAKE2b-256 checksum How to use checksums |
fb205c8f10fa80305266860fa67c9f73b3e26d2f7fca66c937f27c3ab82922f6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 26, 2026.
Transparency logSecurity for s-layer-semantic-formats 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for s_layer_semantic_formats-0.1.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 26, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.