Skip to content

Commit

Permalink
Rewrite it again (#12)
Browse files Browse the repository at this point in the history
* Refactor everything

Signed-off-by: William Woodruff <william@trailofbits.com>

* update note

Signed-off-by: William Woodruff <william@trailofbits.com>

* re-exports

Signed-off-by: William Woodruff <william@trailofbits.com>

* sigstore_rekor_types -> rekor_types

More accurate, shorter.

Signed-off-by: William Woodruff <william@trailofbits.com>

* fixups

Signed-off-by: William Woodruff <william@trailofbits.com>

* cleanup

Signed-off-by: William Woodruff <william@trailofbits.com>

* typing_extensions

Signed-off-by: William Woodruff <william@trailofbits.com>

* use explicit Union

Signed-off-by: William Woodruff <william@trailofbits.com>

---------

Signed-off-by: William Woodruff <william@trailofbits.com>
  • Loading branch information
woodruffw authored Dec 13, 2023
1 parent e4bdb9d commit 49b6826
Show file tree
Hide file tree
Showing 19 changed files with 1,040 additions and 1,125 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
SHELL := /bin/bash

PY_MODULE := sigstore_rekor_types
PY_MODULE := rekor_types

ALL_PY_SRCS := $(shell find $(PY_MODULE) -name '*.py')

Expand Down
63 changes: 40 additions & 23 deletions codegen/codegen.sh
Original file line number Diff line number Diff line change
Expand Up @@ -40,29 +40,46 @@ git clone --quiet \
https://github.com/sigstore/rekor \
"${rekor_dir}"

# NOTE(ww): For whatever reason, the POST endpoint doesn't work. Instead,
# we tell the converter to retrieve the OpenAPI YAML URL itself.
curl -X 'GET' \
"https://converter.swagger.io/api/convert?url=https%3A%2F%2Fraw.githubusercontent.com%2Fsigstore%2Frekor%2F${rekor_ref}%2Fopenapi.yaml" \
-H 'accept: application/json' \
> "${rekor_dir}/openapi.json"

datamodel-codegen \
--input "${rekor_dir}/openapi.json" \
--input-file-type openapi \
--target-python-version 3.8 \
--snake-case-field \
--capitalize-enum-members \
--field-constraints \
--use-schema-description \
--use-subclass-enum \
--disable-timestamp \
--reuse-model \
--use-default-kwarg \
--allow-population-by-field-name \
--strict-types str bytes int float bool \
--output-model-type pydantic_v2.BaseModel \
--output "${pkg_dir}/_internal.py"
# NOTE(ww): Everything below happens because of a confluence of unfortunate
# factors:
#
# * Rekor's top-level `openapi.yaml` is written in OpenAPI 2.0 (because go-swagger
# only supports 2.0)
# * `datamodel-codegen` only supports OpenAPI 3.0+
# * If we convert Rekor's `openapi.yaml` into an OpenAPI 3.0 format, then
# `datamodel-codegen` *works*, but produces suboptimal code (lots
# of duplicated models like `Hash1`, `Hash2`, etc. in the same namespace).
#
# To get around all of this, we poke through each internal JSON Schema definition
# used by Rekor and generate them one-by-one into their own modules.
#
# See:
# * https://github.com/go-swagger/go-swagger/issues/1122
# * https://github.com/koxudaxi/datamodel-code-generator/issues/1590
# * https://github.com/sigstore/rekor/issues/1729
mkdir -p "${pkg_dir}/_internal"
touch "${pkg_dir}/_internal/__init__.py"
rekor_types=(alpine cose dsse hashedrekord helm intoto jar rekord rfc3161 rpm tuf)
for type in "${rekor_types[@]}"; do
dbg "generating models for Rekor type: ${type}"
datamodel-codegen \
--input "${rekor_dir}/pkg/types/${type}/${type}_schema.json" \
--input-file-type jsonschema \
--target-python-version 3.8 \
--collapse-root-models \
--snake-case-field \
--capitalize-enum-members \
--field-constraints \
--use-schema-description \
--use-subclass-enum \
--disable-timestamp \
--reuse-model \
--use-default-kwarg \
--allow-population-by-field-name \
--strict-types str bytes int float bool \
--output-model-type pydantic_v2.BaseModel \
--output "${pkg_dir}/_internal/${type}.py"
done

# Cap it off by auto-reformatting.
make -C "${here}/.." reformat
Expand Down
12 changes: 10 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: Apache Software License",
]
dependencies = ["pydantic[email] >=2,<3"]
dependencies = [
"pydantic[email] >=2,<3",
"typing-extensions; python_version < '3.9'",
]
requires-python = ">=3.8"

[project.optional-dependencies]
Expand All @@ -28,6 +31,8 @@ lint = [
codegen = ["datamodel-code-generator", "sigstore-rekor-types[lint]"]
dev = ["sigstore-rekor-types[codegen,doc,lint]", "build"]

[tool.flit.module]
name = "rekor_types"

[project.urls]
Homepage = "https://pypi.org/project/sigstore-rekor-types"
Expand Down Expand Up @@ -60,7 +65,10 @@ line-length = 100
select = ["ALL"]

[tool.ruff.per-file-ignores]
"sigstore_rekor_types/_internal.py" = [
"rekor_types/__init__.py" = [
"TCH001", # False positive: imports are re-exports, not just for type hints.
]
"rekor_types/_internal/*.py" = [
"A003", # some fields shadow python builtins
"E501", # handled by black, and catches some docstrings we can't autofix
"UP006", # pydantic doesn't support PEP 585 below Python 3.9
Expand Down
130 changes: 130 additions & 0 deletions rekor_types/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
"""The `sigstore_rekor_types` APIs."""

from __future__ import annotations

import sys
from typing import Literal, Union

from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr

from ._internal import (
alpine,
cose,
dsse,
hashedrekord,
helm,
intoto,
jar,
rekord,
rfc3161,
rpm,
tuf,
)

if sys.version_info < (3, 9):
from typing_extensions import Annotated
else:
from typing import Annotated

__version__ = "0.0.11"


class Error(BaseModel):
"""A Rekor server error."""

code: StrictInt
message: StrictStr


class _ProposedEntryMixin(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
api_version: StrictStr = Field(
pattern=r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$",
default="0.0.1",
alias="apiVersion",
)


class Alpine(_ProposedEntryMixin):
"""Proposed entry model for an `alpine` record."""

kind: Literal["alpine"] = "alpine"
spec: alpine.AlpinePackageSchema


class Cose(_ProposedEntryMixin):
"""Proposed entry model for a `cose` record."""

kind: Literal["cose"] = "cose"
spec: cose.CoseSchema


class Dsse(_ProposedEntryMixin):
"""Proposed entry model for a `dsse` record."""

kind: Literal["dsse"] = "dsse"
spec: dsse.DsseSchema


class Hashedrekord(_ProposedEntryMixin):
"""Proposed entry model for a `dsse` record."""

kind: Literal["hashedrekord"] = "hashedrekord"
spec: hashedrekord.RekorSchema


class Helm(_ProposedEntryMixin):
"""Proposed entry model for a `dsse` record."""

kind: Literal["helm"] = "helm"
spec: helm.HelmSchema


class Intoto(_ProposedEntryMixin):
"""Proposed entry model for a `dsse` record."""

kind: Literal["intoto"] = "intoto"
spec: intoto.IntotoSchema


class Jar(_ProposedEntryMixin):
"""Proposed entry model for a `jar` record."""

kind: Literal["jar"] = "jar"
spec: jar.JarSchema


class Rekord(_ProposedEntryMixin):
"""Proposed entry model for a `rekord` record."""

kind: Literal["rekord"] = "rekord"
spec: rekord.RekorSchema


class Rfc3161(_ProposedEntryMixin):
"""Proposed entry model for a `rfc3161` record."""

kind: Literal["rfc3161"] = "rfc3161"
spec: rfc3161.TimestampSchema


class Rpm(_ProposedEntryMixin):
"""Proposed entry model for an `rpm` record."""

kind: Literal["rpm"] = "rpm"
spec: rpm.RpmSchema


class Tuf(_ProposedEntryMixin):
"""Proposed entry model for a `tuf` record."""

kind: Literal["tuf"] = "tuf"
spec: tuf.TufSchema


ProposedEntry = Annotated[
Union[Alpine, Cose, Dsse, Hashedrekord, Helm, Intoto, Jar, Rekord, Rfc3161, Rpm, Tuf],
Field(discriminator="kind"),
]
File renamed without changes.
41 changes: 41 additions & 0 deletions rekor_types/_internal/alpine.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# generated by datamodel-codegen:

from __future__ import annotations

from pydantic import BaseModel, ConfigDict, Field, RootModel


class PublicKey(BaseModel):
"""The public key that can verify the package signature."""

model_config = ConfigDict(
populate_by_name=True,
)
content: str = Field(
...,
description="Specifies the content of the public key inline within the document",
)


class AlpineV001Schema(BaseModel):
"""Schema for Alpine Package entries."""

model_config = ConfigDict(
populate_by_name=True,
)
public_key: PublicKey = Field(
...,
alias="publicKey",
description="The public key that can verify the package signature",
)


class AlpinePackageSchema(RootModel[AlpineV001Schema]):
model_config = ConfigDict(
populate_by_name=True,
)
root: AlpineV001Schema = Field(
...,
description="Schema for Alpine package objects",
title="Alpine Package Schema",
)
84 changes: 84 additions & 0 deletions rekor_types/_internal/cose.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# generated by datamodel-codegen:

from __future__ import annotations

from enum import Enum
from typing import Optional

from pydantic import BaseModel, ConfigDict, Field, RootModel, StrictStr


class Algorithm(str, Enum):
"""The hashing function used to compute the hash value."""

SHA256 = "sha256"


class PayloadHash(BaseModel):
"""Specifies the hash algorithm and value for the content."""

model_config = ConfigDict(
populate_by_name=True,
)
algorithm: Algorithm = Field(
...,
description="The hashing function used to compute the hash value",
)
value: StrictStr = Field(..., description="The hash value for the content")


class EnvelopeHash(BaseModel):
"""Specifies the hash algorithm and value for the COSE envelope."""

model_config = ConfigDict(
populate_by_name=True,
)
algorithm: Algorithm = Field(
...,
description="The hashing function used to compute the hash value",
)
value: StrictStr = Field(..., description="The hash value for the envelope")


class Data(BaseModel):
"""Information about the content associated with the entry."""

model_config = ConfigDict(
populate_by_name=True,
)
payload_hash: Optional[PayloadHash] = Field(
default=None,
alias="payloadHash",
description="Specifies the hash algorithm and value for the content",
)
envelope_hash: Optional[EnvelopeHash] = Field(
default=None,
alias="envelopeHash",
description="Specifies the hash algorithm and value for the COSE envelope",
)
aad: Optional[str] = Field(
default=None,
description="Specifies the additional authenticated data required to verify the signature",
)


class CoseV001Schema(BaseModel):
"""Schema for cose object."""

model_config = ConfigDict(
populate_by_name=True,
)
message: Optional[str] = Field(default=None, description="The COSE Sign1 Message")
public_key: str = Field(
...,
alias="publicKey",
description="The public key that can verify the signature",
)
data: Data = Field(..., description="Information about the content associated with the entry")


class CoseSchema(RootModel[CoseV001Schema]):
model_config = ConfigDict(
populate_by_name=True,
)
root: CoseV001Schema = Field(..., description="COSE for Rekord objects", title="COSE Schema")
Loading

0 comments on commit 49b6826

Please sign in to comment.