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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -255,3 +255,8 @@ dependency_links.txt
top_level.txt

.tox/

# The OWID source copied into the 51Did package at build time by
# ci/copy-owid-source.ps1. The submodule is the only copy that belongs in
# the repository.
fiftyone_pipeline_did/src/fiftyone_pipeline_did/_owid/
6 changes: 6 additions & 0 deletions ci/build-package.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ param (
[string]$Version
)

# The 51Did package carries the OWID source inside itself, because the
# dependency cannot come from a package registry. Copy it in before the
# distributions are built, so that both the source distribution and the wheel
# contain it. See ci/copy-owid-source.ps1 for the full reasoning.
& "$PSScriptRoot/copy-owid-source.ps1" -RepoRoot $RepoName

$packages = "fiftyone_pipeline_core", "fiftyone_pipeline_engines", "fiftyone_pipeline_engines_fiftyone", "fiftyone_pipeline_cloudrequestengine", "fiftyone_pipeline_translation", "fiftyone_pipeline_did"
./python/build-package-pypi.ps1 -RepoName $RepoName -Version $Version -Packages $packages

Expand Down
5 changes: 5 additions & 0 deletions ci/build-project.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ param (
[string]$RepoName
)

# The 51Did package is built from its own tox environment by the unit test
# step, and that build needs the OWID source in place, so put it there before
# anything builds. See ci/copy-owid-source.ps1 for the full reasoning.
& "$PSScriptRoot/copy-owid-source.ps1" -RepoRoot $RepoName

$packages = "fiftyone_pipeline_core", "fiftyone_pipeline_engines", "fiftyone_pipeline_engines_fiftyone", "fiftyone_pipeline_cloudrequestengine", "fiftyone_pipeline_translation"
./python/build-project.ps1 -RepoName $RepoName -Packages $packages

Expand Down
76 changes: 76 additions & 0 deletions ci/copy-owid-source.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
param (
# The root of the pipeline-python working copy. Defaults to the parent of
# this script, which is right for a developer checkout, and is passed
# explicitly by the CI scripts because they run from the workspace above
# the clone.
[string]$RepoRoot = (Split-Path $PSScriptRoot -Parent)
)
$ErrorActionPreference = "Stop"

# Copies the OWID source into the 51Did package as the private module
# fiftyone_pipeline_did._owid, so that a published wheel can be imported
# without the OWID library being installed separately.
#
# The dependency cannot come from a package registry. The 51Degrees fork of
# OWID is not published to PyPI, and the name "owid" on PyPI belongs to an
# unrelated project, so declaring install_requires=["owid"] would install the
# wrong thing. The name here carries a leading underscore so that installing
# the 51Did package never claims the top level name "owid" on a consumer's
# machine.
#
# Nothing is fetched over the network, because owid-python is a submodule and
# CI clones with --recurse-submodules. Nothing is written back to the
# repository either, as the copy is ignored by git and main keeps the
# submodule as the single source of the OWID code.

$owidRepo = Join-Path $RepoRoot "owid-python"
$owidSource = Join-Path $owidRepo "owid"
$target = Join-Path $RepoRoot `
"fiftyone_pipeline_did/src/fiftyone_pipeline_did/_owid"

if (-not (Test-Path (Join-Path $owidSource "__init__.py"))) {
throw "OWID source not found at '$owidSource'. Run " +
"'git submodule update --init --recursive' first, or clone with " +
"--recurse-submodules."
}

# The commit the copy was taken from, so the notice can say exactly which
# version of the OWID source is inside the package.
$commit = (git -C $owidRepo rev-parse HEAD 2>$null)
if (-not $commit) {
$commit = (git -C $RepoRoot rev-parse "HEAD:owid-python" 2>$null)
}
if (-not $commit) {
throw "Could not determine the owid-python commit to record in the notice."
}

if (Test-Path $target) {
Remove-Item -Path $target -Recurse -Force
}
$null = New-Item -ItemType Directory -Path $target -Force

Copy-Item -Path (Join-Path $owidSource "*.py") -Destination $target -Force
Copy-Item -Path (Join-Path $owidRepo "LICENSE") `
-Destination (Join-Path $target "LICENSE") -Force

$notice = @"
The Python modules in this directory are the OWID (Open Web Id) library. They
are copied into the fiftyone_pipeline_did package at build time and are not
part of the 51Degrees source, so they keep their own licence, which is the
Apache License 2.0 in the LICENSE file beside this notice, and not the EUPL
1.2 that covers the rest of the package.

Copyright 2026 51 Degrees Mobile Experts Limited (51degrees.com)

Taken from the 51Degrees fork of the OWID project,
https://github.com/51Degrees/owid-python, at commit
$commit
which follows https://github.com/SWAN-community/owid-python.

The modules are placed under the private name _owid so that installing this
package does not claim the top level name "owid", which on PyPI belongs to an
unrelated project. Import OWID from the fork itself rather than from here.
"@
Set-Content -Path (Join-Path $target "NOTICE") -Value $notice -Encoding utf8

Write-Output "Copied OWID source at $commit into '$target'"
5 changes: 5 additions & 0 deletions fiftyone_pipeline_did/MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
include version.txt
include readme.md
# The OWID source carried inside this package under the private name _owid is
# Apache-2.0, so its licence and the notice naming where the source came from
# have to travel with it in both the sdist and the wheel.
include src/fiftyone_pipeline_did/_owid/LICENSE
include src/fiftyone_pipeline_did/_owid/NOTICE
5 changes: 3 additions & 2 deletions fiftyone_pipeline_did/examples/creator_context_web/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,9 @@
# The package from this repository rather than a published one, so the
# branch is what runs. When the package is installed the import succeeds
# directly, and otherwise the package source beside this example is used,
# which is how a checkout runs the demo without an install step. The OWID
# library the package builds on must be installed either way (see the
# which is how a checkout runs the demo without an install step. Running
# from the source needs the OWID copy that ci/copy-owid-source.ps1 puts in
# place, so run pwsh ./setup.ps1 from the repository root first (see the
# package readme).
try:
from fiftyone_pipeline_did import (
Expand Down
31 changes: 28 additions & 3 deletions fiftyone_pipeline_did/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,34 @@ as `PROBABILISTIC`.

`FodId` builds on the OWID envelope library
([SWAN-community/owid-python](https://github.com/SWAN-community/owid-python),
package `owid`), consumed via the `51Degrees/owid-python` fork (git submodule;
switch to upstream once published). `Owid` is composed, not subclassed:
`FodId` holds an `Owid` and delegates OWID-level concerns to it.
package `owid`), consumed via the
[51Degrees/owid-python](https://github.com/51Degrees/owid-python) fork, which
is a git submodule of this repository and will move to upstream once that is
published. `Owid` is composed, not subclassed, so `FodId` holds an `Owid` and
delegates OWID-level concerns to it.

The published package does not take OWID from PyPI. The 51Degrees fork is not
published there, and the name `owid` on PyPI belongs to an unrelated project,
so a declared dependency would install the wrong thing. Instead
`ci/copy-owid-source.ps1` copies the fork's source into the package as the
private module `fiftyone_pipeline_did._owid` before the distribution is built,
carrying the Apache-2.0 licence and a notice naming the source commit with it.
The leading underscore keeps the top level name `owid` free on the consumer's
machine. The only third party requirement the package declares is
`cryptography`, which OWID uses for the signatures.

Nothing changes for a developer working in this repository, because the
submodule stays where it is and the same script puts the copy in place.
Run `pwsh ./setup.ps1` from the repository root, or
`git submodule update --init --recursive` followed by
`pwsh ./ci/copy-owid-source.ps1`, before building or running the tests. The
tests and examples import the fork under its own name, `owid`, which is how
they build signed envelopes to test against.

The two OWID types the public API refers to are re-exported from the package
itself, so a caller never has to reach into the private module. Catch
`fiftyone_pipeline_did.OwidError` for an OWID level failure, and use
`fiftyone_pipeline_did.Owid` for the envelope that `FodId.from_owid` takes.

## Usage

Expand Down
23 changes: 14 additions & 9 deletions fiftyone_pipeline_did/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,21 @@ def read(file_name):
long_description=read("readme.md"),
long_description_content_type='text/markdown',
python_requires=">=3.9",
packages=["fiftyone_pipeline_did"],
packages=["fiftyone_pipeline_did", "fiftyone_pipeline_did._owid"],
package_dir={"": "src"},
# OWID is provided by the 51Degrees owid-python fork, which is consumed via
# the git submodule / editable install (see tox.ini) rather than declared as
# a PyPI dependency. The name "owid" on PyPI belongs to an unrelated project,
# and the 51Degrees fork is not published to PyPI, so a bare
# install_requires=['owid'] would resolve to the wrong package. If this
# package is ever published standalone, depend on a 51Degrees-namespaced
# OWID distribution here instead.
install_requires=[],
# The OWID source is carried inside this package as the private module
# fiftyone_pipeline_did._owid, copied out of the 51Degrees owid-python
# fork by ci/copy-owid-source.ps1 before the distribution is built. It
# cannot
# come from a package registry, because the 51Degrees fork is not
# published to PyPI and the name "owid" there belongs to an unrelated
# project, so a bare install_requires=["owid"] would install the wrong
# thing. The module is private (leading underscore) so that installing
# this package never claims the top level name "owid" on a consumer's
# machine. Its only third party requirement is cryptography, which is
# declared below and does come from PyPI.
package_data={"fiftyone_pipeline_did._owid": ["LICENSE", "NOTICE"]},
install_requires=["cryptography>=41"],
license="EUPL-1.2",
classifiers=[
"Development Status :: 5 - Production/Stable",
Expand Down
8 changes: 8 additions & 0 deletions fiftyone_pipeline_did/src/fiftyone_pipeline_did/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@
SignatureReason,
SignatureResult,
)
# The OWID library is carried inside this package as the private module
# _owid, because it cannot be installed from a package registry (see the
# package readme). The envelope type and the error type are re-exported here
# so that callers have supported names for the two of them that the public
# API refers to, as the private module itself is not part of that API.
from ._owid import Owid, OwidError
from .fod_id import DATE_EPOCH, FodId
from .id_type import IdType

Expand All @@ -71,4 +77,6 @@
"DidArgumentError",
"DidNotSupportedError",
"DEFAULT_ENDPOINT",
"Owid",
"OwidError",
]
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
from enum import Enum
from typing import Any, Callable, Dict, List, Optional, Tuple, Union

from owid import Version
from ._owid import Version

from .fod_id import DATE_EPOCH, FodId
from .id_type import IdType
Expand Down
21 changes: 12 additions & 9 deletions fiftyone_pipeline_did/src/fiftyone_pipeline_did/fod_id.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

from datetime import datetime, timezone

from owid import Owid, Version
from ._owid import Owid, Version

from .id_type import IdType

Expand All @@ -39,8 +39,9 @@ class FodId:
returned by the 51Degrees Cloud service.

A 51Did is described at three levels. The **51Did** is the identifier as a
whole. The **envelope** is the signed :class:`owid.Owid` that carries it
(version, domain, date, payload, signature), re-issued fresh on every call.
whole. The **envelope** is the signed
:class:`~fiftyone_pipeline_did.Owid` that carries it (version, domain,
date, payload, signature), re-issued fresh on every call.
The **value** is the stable, comparable part of the payload after the Flags
and License Id, exposed as :attr:`hash`. Two 51Dids for the same inputs
share the same value even though their envelopes differ. *Compare values,
Expand Down Expand Up @@ -77,8 +78,8 @@ class FodId:
PAYLOAD_LENGTH = HASH_OFFSET + HASH_LENGTH

def __init__(self, owid: Owid) -> None:
"""Promotes an already-parsed :class:`owid.Owid` into a 51Did by
unpacking its payload.
"""Promotes an already-parsed :class:`~fiftyone_pipeline_did.Owid`
into a 51Did by unpacking its payload.

The OWID is **copied** (round-tripped through its byte form), not
aliased, so a ``FodId`` can never desync from its envelope if the caller
Expand All @@ -87,8 +88,8 @@ def __init__(self, owid: Owid) -> None:

Raises :class:`TypeError` if ``owid`` is ``None``, :class:`ValueError`
if the payload is shorter than the minimum for its identifier type, and
:class:`owid.OwidError` if the OWID cannot be serialized (e.g. it is
unsigned).
:class:`~fiftyone_pipeline_did.OwidError` if the OWID cannot be
serialized (e.g. it is unsigned).
"""
if owid is None:
raise TypeError("owid must not be None")
Expand Down Expand Up @@ -141,7 +142,8 @@ def from_base64(cls, base64: str) -> "FodId":
never converts an identifier it received from a link.

Raises :class:`TypeError` if ``base64`` is ``None`` and
:class:`owid.OwidError` if it is not valid base64 or not a valid OWID.
:class:`~fiftyone_pipeline_did.OwidError` if it is not valid base64
or not a valid OWID.
"""
if base64 is None:
raise TypeError("base64 must not be None")
Expand Down Expand Up @@ -177,7 +179,8 @@ def from_byte_array(cls, buffer: bytes) -> "FodId":
"""Parses a 51Did from the raw bytes of an OWID envelope.

Raises :class:`TypeError` if ``buffer`` is ``None`` and
:class:`owid.OwidError` if the bytes are not a valid OWID.
:class:`~fiftyone_pipeline_did.OwidError` if the bytes are not a
valid OWID.
"""
if buffer is None:
raise TypeError("buffer must not be None")
Expand Down
4 changes: 2 additions & 2 deletions fiftyone_pipeline_did/tests/test_fodid.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@
import unittest
from datetime import datetime, timezone

from owid import Owid, Crypto, Creator, OwidError
from owid import Owid, Crypto, Creator

from fiftyone_pipeline_did import FodId, IdType
from fiftyone_pipeline_did import FodId, IdType, OwidError

TEST_DOMAIN = "51degrees.com"
# 0xA5: usage bits plus the HashedEmail type tag in bits 6-7.
Expand Down
6 changes: 6 additions & 0 deletions setup.ps1
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
# The 51Did package builds against the OWID source, which lives in the
# owid-python submodule and is copied into the package rather than installed
# from a package registry. See ci/copy-owid-source.ps1 for why.
& "$PSScriptRoot/ci/copy-owid-source.ps1"

python -m pip install -e fiftyone_pipeline_cloudrequestengine/
python -m pip install -e fiftyone_pipeline_core/
python -m pip install -e fiftyone_pipeline_translation/
python -m pip install -e fiftyone_pipeline_engines/
python -m pip install -e fiftyone_pipeline_engines_fiftyone/
python -m pip install -e owid-python/
python -m pip install flask
Loading