Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 8 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,45 +1,18 @@
# MailSchema for Python
# mailschema

[![PyPI](https://img.shields.io/pypi/v/mailschema)](https://pypi.org/project/mailschema/)
[![CI](https://github.com/mailschema/python/actions/workflows/test.yml/badge.svg)](https://github.com/mailschema/python/actions/workflows/test.yml)

The Mail Action Protocol 0.2 core artifacts, and Draft 2020-12 validation of MailSchema Registry contributions, with no network requests.

[Specification](https://mailschema.org/specification) · [Registry](https://mailschema.org/registry) · [Tools](https://mailschema.org/tools) · [Source](https://github.com/mailschema/python)

## Install
The [Mail Action Protocol](https://mailschema.org) 0.3 artifacts for Python, byte for byte as mailschema.org publishes them: the profile record, the JSON-LD context, and the core, type contract and implementation record schemas. The profile record binds the context and the first two schemas by SHA-256. The package has no dependencies.

```sh
pip install mailschema
```

Python 3.10 or newer is required.

## MAP 0.2 core artifacts

```python
from mailschema import MAP_PROFILE, get_contract_format_schema, get_forms_schema, get_map_context, get_map_schema
pip install mailschema==0.3.0
```

Each returns a fresh copy of the file the [profile record](https://mailschema.org/profiles/map/0.2.json) binds by SHA-256. Type contracts are not bundled: a client obtains them from the [Registry catalogue](https://mailschema.org/registry/catalog.json) by digest. Processing MAP messages is outside this package; see the [profile](https://mailschema.org/specification/profile).

## Registry data

```python
from mailschema import contribution_errors, validate_contribution
import json
import mailschema

errors = contribution_errors(candidate)
validate_contribution(candidate) # Raises ValueError for invalid input.
```

`validate_record`, `record_errors` and `get_record_schema` handle expanded Registry records.

```sh
python -m mailschema check contribution.json
python -m mailschema check record.json --record
python -m mailschema schema --map
core_schema = json.loads(mailschema.artifact("core.schema.json"))
```

## Trust boundary
`mailschema.ARTIFACTS` lists the bundled files and `mailschema.PROFILE` names the profile. To parse and check descriptions and contracts, use the JavaScript or Ruby package, or implement the [specification](https://mailschema.org/specification/core); schema validation alone does not establish a valid description.

A valid document is structured input. Validation does not authenticate a service, grant authority or establish product conformance. The CLI reads one local file of at most 256 KiB and does not upload or modify it. Package versions and MAP profile versions advance independently. MIT licensed.
Source: [mailschema/python](https://github.com/mailschema/python). License: MIT.
16 changes: 8 additions & 8 deletions UPSTREAM.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# Release provenance

Version `0.2.1` derives from [`mailschema/mailschema@db99c15`](https://github.com/mailschema/mailschema/commit/db99c1582f68b3cd3e80d5fe6c029c8535bf470c).
Version `0.3.0` derives from [`mailschema/mailschema@2c26469`](https://github.com/mailschema/mailschema/commit/2c26469209070f04ff9f8ec29e1387b8699a46a0). Its sources, tests and bundled files are prepared from that commit by `npm run packages:prepare`.

The bundled files are exact projections of that source commit and do not independently define MAP:
The bundled MAP 0.3 artifacts are exact copies of that commit's files and do not independently define MAP:

- MAP 0.2 core schema: `732d60fcb758d945dc15c57d885c9d1c0c76423a7439096d9755e8ec9830836a`
- MAP 0.2 JSON-LD context: `dfed0189524d7d16d31ed3d8af97e9a0c458200c4c034b413f9d14fa35b60257`
- Type contract format: `e030007cc034662c9d9d448a8a035aff3106cfb96fb60e6aa9f92b99b25df0b9`
- Form fields block: `3a976b93a057380e3630ca05e77e221a43a98559fb7fda8066299ec4bf2a8f9e`
- Contribution schema: `a8a241b4837971a6251bcc135c704283536f4fb825fbbaedaf1598c6e688d564`
- profile (`mailschema/profile.json`): `d9481627f855c127a4f5d6ddb7f3b699f66f6475bc8672d609f5b499f83d1a0f`
- context (`mailschema/context.jsonld`): `a51490d1fe8af695c14ea508aad88fd0429d18564791b6b52f3ab07d5c51aab3`
- core-schema (`mailschema/core.schema.json`): `0ad09a6955a47e7a2c1c1eec0747a3e0b15c08a79860e90ce4e65da11983aeb3`
- contract-schema (`mailschema/contract.schema.json`): `86bf057529c4fd2c7f0a880825774afeed31e9f7ba1d99760858a585fe28d18d`
- implementation-schema (`mailschema/implementation.schema.json`): `c62acc3ebb9f384c8fba7be353ab1905b65883ac2b16b147fd98485eec643396`

The canonical artifacts live in the main MailSchema repository. This repository owns the Python package surface and its release history.
The canonical artifacts live in the main MailSchema repository. This repository owns the Python package and its release history.
9 changes: 3 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ build-backend = "hatchling.build"

[project]
name = "mailschema"
version = "0.2.1"
description = "Mail Action Protocol 0.2 core artifacts and MailSchema Registry validation"
version = "0.3.0"
description = "Mail Action Protocol 0.3 artifacts: the profile record, context and schemas"
readme = "README.md"
license = "MIT"
license-files = ["LICENSE"]
requires-python = ">=3.10"
authors = [{ name = "MailSchema contributors" }]
dependencies = ["jsonschema[format]>=4.23,<5"]
dependencies = []
classifiers = [
"Development Status :: 3 - Alpha",
"Programming Language :: Python :: 3",
Expand All @@ -23,8 +23,5 @@ Homepage = "https://mailschema.org/tools"
Repository = "https://github.com/mailschema/python"
Issues = "https://github.com/mailschema/python/issues"

[project.scripts]
mailschema = "mailschema.__main__:main"

[tool.hatch.build.targets.wheel]
packages = ["src/mailschema"]
108 changes: 23 additions & 85 deletions src/mailschema/__init__.py
Original file line number Diff line number Diff line change
@@ -1,93 +1,31 @@
"""The MAP 0.2 core artifacts and JSON Schema validation for MailSchema Registry contributions."""
"""Mail Action Protocol 0.3 artifacts, byte for byte as mailschema.org publishes them.

import json
from importlib.resources import files
from typing import Any

from jsonschema import Draft202012Validator, FormatChecker

__version__ = "0.2.1"

MAP_PROFILE = "https://mailschema.org/profiles/map/0.2"
"""The MAP profile whose core artifacts this package carries."""


def _bundled(name: str) -> dict[str, Any]:
return json.loads(files(__package__).joinpath(name).read_text(encoding="utf-8"))


def get_map_schema() -> dict[str, Any]:
"""Return a fresh copy of the MAP 0.2 core schema, as the profile record binds it by SHA-256."""
return _bundled("map-0.2.schema.json")


def get_map_context() -> dict[str, Any]:
"""Return a fresh copy of the MAP 0.2 JSON-LD context."""
return _bundled("map-0.2.jsonld")


def get_contract_format_schema() -> dict[str, Any]:
"""Return a fresh copy of the type contract format every MAP 0.2 contract follows."""
return _bundled("type-contract-0.2.schema.json")
The profile record, the JSON-LD context, and the core, type contract and implementation record
schemas. The profile record binds the context and the first two schemas by SHA-256.
"""

from importlib.resources import files

def get_forms_schema() -> dict[str, Any]:
"""Return a fresh copy of the form fields block contracts pin."""
return _bundled("forms-0.1.schema.json")


def get_contribution_schema() -> dict[str, Any]:
"""Return a fresh copy of the bundled Registry contribution schema."""
return _bundled("contribution.schema.json")


def get_record_schema() -> dict[str, Any]:
"""Return the schema for expanded Registry records."""
schema = get_contribution_schema()
return {"$schema": schema["$schema"], "$defs": schema["$defs"], "$ref": "#/$defs/record"}


_schema = get_contribution_schema()
_validator = Draft202012Validator(_schema, format_checker=FormatChecker())
_variants = {
variant["properties"]["kind"]["const"]: Draft202012Validator(
{"$schema": _schema["$schema"], "$defs": _schema["$defs"], **variant},
format_checker=FormatChecker(),
)
for variant in _schema["oneOf"]
}
_record_validator = Draft202012Validator(get_record_schema(), format_checker=FormatChecker())


def _errors(validator: Draft202012Validator, value: Any) -> list[str]:
result = []
for error in validator.iter_errors(value):
path = "/" + "/".join(str(part).replace("~", "~0").replace("/", "~1") for part in error.absolute_path)
result.append(f"{path} {error.message}")
return result


def contribution_errors(value: Any) -> list[str]:
"""Return structural errors; an empty list means the schema checks passed."""
kind = value.get("kind") if isinstance(value, dict) else None
validator = _variants.get(kind, _validator) if isinstance(kind, str) else _validator
return _errors(validator, value)

__version__ = "0.3.0"

def record_errors(value: Any) -> list[str]:
"""Return structural errors for a Registry record."""
return _errors(_record_validator, value)
PROFILE = "https://mailschema.org/profiles/map/0.3"
"""The MAP profile these artifacts define."""

CONTEXT = "https://mailschema.org/contexts/map-0.3.jsonld"
"""The JSON-LD context every MAP 0.3 description names."""

def validate_contribution(value: Any) -> None:
"""Raise ValueError if a contribution does not match the schema."""
errors = contribution_errors(value)
if errors:
raise ValueError("Invalid contribution:\n" + "\n".join(errors))
ARTIFACTS = (
"profile.json",
"context.jsonld",
"core.schema.json",
"contract.schema.json",
"implementation.schema.json",
)
"""The bundled files, by name."""


def validate_record(value: Any) -> None:
"""Raise ValueError if a Registry record does not match the schema."""
errors = record_errors(value)
if errors:
raise ValueError("Invalid type record:\n" + "\n".join(errors))
def artifact(name: str) -> bytes:
"""Return the exact bytes of a bundled artifact."""
if name not in ARTIFACTS:
raise KeyError(f"mailschema: unknown artifact {name!r}")
return files(__package__).joinpath(name).read_bytes()
57 changes: 0 additions & 57 deletions src/mailschema/__main__.py

This file was deleted.

50 changes: 50 additions & 0 deletions src/mailschema/context.jsonld
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"@context": {
"@version": 1.1,
"@protected": true,
"@vocab": "https://mailschema.org/vocab/map/0.3#",
"MailAction": "https://mailschema.org/vocab/map/0.3#MailAction",
"profile": {
"@id": "profile",
"@type": "@id"
},
"type": {
"@id": "type",
"@type": "@json"
},
"service": {
"@id": "service",
"@type": "@json"
},
"subject": {
"@id": "subject",
"@type": "@json"
},
"terms": {
"@id": "terms",
"@type": "@json"
},
"details": {
"@id": "details",
"@type": "@json"
},
"operations": {
"@id": "operations",
"@type": "@json"
},
"human": {
"@id": "human",
"@type": "@json"
},
"recipient": "recipient",
"inLanguage": "inLanguage",
"issuedAt": {
"@id": "issuedAt",
"@type": "http://www.w3.org/2001/XMLSchema#dateTime"
},
"expiresAt": {
"@id": "expiresAt",
"@type": "http://www.w3.org/2001/XMLSchema#dateTime"
}
}
}
Loading
Loading