Compare commits

..

3 Commits

Author SHA1 Message Date
08c1d0c605 feat: Added config validation 2026-02-20 05:33:50 +01:00
7b29763230 feat: Add yaml schema pre-commit check to patches 2026-02-20 05:21:59 +01:00
95de53206e feat: Add schema comments to patches
These comments ensure that we get proper yaml language server support
in the patch files.
Also fixes all the resulting language server errors.
2026-02-20 05:19:59 +01:00
34 changed files with 3112 additions and 44 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
.ipxe/ .ipxe/
rendered/ rendered/
configs/ configs/
*.egg-info

View File

@@ -12,6 +12,12 @@ repos:
- id: check-merge-conflict - id: check-merge-conflict
- id: check-executables-have-shebangs - id: check-executables-have-shebangs
- repo: https://github.com/jmlrt/check-yamlschema
rev: v0.0.7
hooks:
- id: check-yamlschema
files: ^patches/.*\.yaml$
- repo: https://github.com/crate-ci/typos - repo: https://github.com/crate-ci/typos
rev: v1.40.0 rev: v1.40.0
hooks: hooks:

View File

@@ -1,2 +1,3 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/siderolabs/talos/refs/heads/release-1.11/website/content/v1.11/schemas/config.schema.json
cluster: cluster:
allowSchedulingOnControlPlanes: true allowSchedulingOnControlPlanes: true

View File

@@ -1,3 +1,4 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/siderolabs/talos/refs/heads/release-1.11/website/content/v1.11/schemas/config.schema.json
machine: machine:
features: features:
hostDNS: hostDNS:

View File

@@ -1,16 +1,20 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/siderolabs/talos/refs/heads/release-1.11/website/content/v1.11/schemas/config.schema.json
cluster: cluster:
inlineManifests: inlineManifests:
- name: cluster-variables - name: cluster-variables
contents: | contents: |
---
# yaml-language-server: $schema=https://raw.githubusercontent.com/yannh/kubernetes-json-schema/refs/heads/master/v1.34.1-standalone-strict/namespace.json
apiVersion: v1 apiVersion: v1
kind: Namespace kind: Namespace
metadata: metadata:
name: flux-system name: flux-system
--- ---
# yaml-language-server: $schema=https://raw.githubusercontent.com/yannh/kubernetes-json-schema/refs/heads/master/v1.34.1-standalone-strict/configmap.json
apiVersion: v1 apiVersion: v1
kind: ConfigMap kind: ConfigMap
metadata: metadata:
name: cluster-variables name: cluster-variables
namespace: flux-system namespace: flux-system
data: data:
cluster_env: {%- if node.cluster.production %} production {%- else %} staging {%- endif %} cluster_env: "{%- if node.cluster.production %} production {%- else %} staging {%- endif %}"

View File

@@ -1,3 +1,4 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/siderolabs/talos/refs/heads/release-1.11/website/content/v1.11/schemas/config.schema.json
cluster: cluster:
extraManifests: extraManifests:
- https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.4.1/standard-install.yaml - https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.4.1/standard-install.yaml

View File

@@ -1,3 +1,4 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/siderolabs/talos/refs/heads/release-1.11/website/content/v1.11/schemas/config.schema.json
machine: machine:
network: network:
hostname: {{node.hostname}} hostname: "{{node.hostname}}"

View File

@@ -1,3 +1,4 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/siderolabs/talos/refs/heads/release-1.11/website/content/v1.11/schemas/config.schema.json
machine: machine:
install: install:
disk: {{node.installDisk}} disk: "{{node.installDisk}}"

View File

@@ -1,3 +1,4 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/siderolabs/talos/refs/heads/release-1.11/website/content/v1.11/schemas/config.schema.json
apiVersion: v1alpha1 apiVersion: v1alpha1
kind: VolumeConfig kind: VolumeConfig
name: EPHEMERAL name: EPHEMERAL

View File

@@ -1,3 +1,4 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/siderolabs/talos/refs/heads/release-1.11/website/content/v1.11/schemas/config.schema.json
apiVersion: v1alpha1 apiVersion: v1alpha1
kind: UserVolumeConfig kind: UserVolumeConfig
name: local-path-provisioner name: local-path-provisioner

View File

@@ -1,3 +1,4 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/siderolabs/talos/refs/heads/release-1.11/website/content/v1.11/schemas/config.schema.json
apiVersion: v1alpha1 apiVersion: v1alpha1
kind: UserVolumeConfig kind: UserVolumeConfig
name: longhorn name: longhorn

View File

@@ -1,3 +1,4 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/siderolabs/talos/refs/heads/release-1.11/website/content/v1.11/schemas/config.schema.json
machine: machine:
kubelet: kubelet:
extraMounts: extraMounts:

View File

@@ -1,3 +1,4 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/siderolabs/talos/refs/heads/release-1.11/website/content/v1.11/schemas/config.schema.json
cluster: cluster:
extraManifests: extraManifests:
- https://raw.githubusercontent.com/alex1989hu/kubelet-serving-cert-approver/main/deploy/standalone-install.yaml - https://raw.githubusercontent.com/alex1989hu/kubelet-serving-cert-approver/main/deploy/standalone-install.yaml

View File

@@ -1,3 +1,4 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/siderolabs/talos/refs/heads/release-1.11/website/content/v1.11/schemas/config.schema.json
machine: machine:
kubelet: kubelet:
extraArgs: extraArgs:

View File

@@ -1,10 +1,11 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/siderolabs/talos/refs/heads/release-1.11/website/content/v1.11/schemas/config.schema.json
machine: machine:
network: network:
interfaces: interfaces:
- interface: {{node.interface}} - interface: "{{node.interface}}"
dhcp: false dhcp: false
addresses: addresses:
- {{node.ip}} - "{{node.ip}}"
routes: routes:
- network: 0.0.0.0/0 - network: 0.0.0.0/0
gateway: {{node.gateway}} gateway: "{{node.gateway}}"

View File

@@ -1,3 +1,4 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/siderolabs/talos/refs/heads/release-1.11/website/content/v1.11/schemas/config.schema.json
machine: machine:
# This is only needed on nodes that will have storage # This is only needed on nodes that will have storage
sysctls: sysctls:

View File

@@ -1,3 +1,4 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/siderolabs/talos/refs/heads/release-1.11/website/content/v1.11/schemas/config.schema.json
cluster: cluster:
inlineManifests: inlineManifests:
- name: sops-key - name: sops-key

View File

@@ -1,3 +1,4 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/siderolabs/talos/refs/heads/release-1.11/website/content/v1.11/schemas/config.schema.json
machine: machine:
files: files:
- path: /etc/cri/conf.d/20-customization.part - path: /etc/cri/conf.d/20-customization.part

View File

@@ -1,3 +1,4 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/siderolabs/talos/refs/heads/release-1.11/website/content/v1.11/schemas/config.schema.json
apiVersion: v1alpha1 apiVersion: v1alpha1
kind: ExtensionServiceConfig kind: ExtensionServiceConfig
name: tailscale name: tailscale

View File

@@ -1,6 +1,7 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/siderolabs/talos/refs/heads/release-1.11/website/content/v1.11/schemas/config.schema.json
machine: machine:
network: network:
interfaces: interfaces:
- interface: {{node.interface}} - interface: "{{node.interface}}"
vip: vip:
ip: {{node.cluster.controlPlaneIp}} ip: "{{node.cluster.controlPlaneIp}}"

View File

@@ -9,6 +9,9 @@ dependencies = [
"jinja2==3.1.6", "jinja2==3.1.6",
"mergedeep==1.3.4", "mergedeep==1.3.4",
"netaddr==1.3.0", "netaddr==1.3.0",
"pydantic>=2.12.5",
"pydantic-extra-types>=2.11.0",
"pyyaml==6.0.3", "pyyaml==6.0.3",
"requests==2.32.5", "requests==2.32.5",
"semver>=3.0.4",
] ]

62
src/models/__init__.py Normal file
View File

@@ -0,0 +1,62 @@
# generated by datamodel-codegen:
# filename: schema.tRo.json
# timestamp: 2026-02-20T04:31:38+00:00
from __future__ import annotations
from pydantic import RootModel
from . import (
block,
extensions,
hardware,
network,
runtime,
security,
siderolink,
v1alpha1,
)
class Model(
RootModel[
block.ExistingVolumeConfigV1Alpha1
| block.RawVolumeConfigV1Alpha1
| block.SwapVolumeConfigV1Alpha1
| block.UserVolumeConfigV1Alpha1
| block.VolumeConfigV1Alpha1
| block.ZswapConfigV1Alpha1
| extensions.ServiceConfigV1Alpha1
| hardware.PCIDriverRebindConfigV1Alpha1
| network.DefaultActionConfigV1Alpha1
| network.EthernetConfigV1Alpha1
| network.KubespanEndpointsConfigV1Alpha1
| network.RuleConfigV1Alpha1
| runtime.EventSinkV1Alpha1
| runtime.KmsgLogV1Alpha1
| runtime.WatchdogTimerV1Alpha1
| security.TrustedRootsConfigV1Alpha1
| siderolink.ConfigV1Alpha1
| v1alpha1.Config
]
):
root: (
block.ExistingVolumeConfigV1Alpha1
| block.RawVolumeConfigV1Alpha1
| block.SwapVolumeConfigV1Alpha1
| block.UserVolumeConfigV1Alpha1
| block.VolumeConfigV1Alpha1
| block.ZswapConfigV1Alpha1
| extensions.ServiceConfigV1Alpha1
| hardware.PCIDriverRebindConfigV1Alpha1
| network.DefaultActionConfigV1Alpha1
| network.EthernetConfigV1Alpha1
| network.KubespanEndpointsConfigV1Alpha1
| network.RuleConfigV1Alpha1
| runtime.EventSinkV1Alpha1
| runtime.KmsgLogV1Alpha1
| runtime.WatchdogTimerV1Alpha1
| security.TrustedRootsConfigV1Alpha1
| siderolink.ConfigV1Alpha1
| v1alpha1.Config
)

403
src/models/block.py Normal file
View File

@@ -0,0 +1,403 @@
# generated by datamodel-codegen:
# filename: schema.tRo.json
# timestamp: 2026-02-20T04:31:38+00:00
from __future__ import annotations
from enum import Enum
from pydantic import BaseModel, ConfigDict, Field
class DiskSelector(BaseModel):
model_config = ConfigDict(
extra='forbid',
)
match: str | None = Field(
None,
description='The Common Expression Language (CEL) expression to match the disk.\n',
title='match',
)
class EncryptionKeyKMS(BaseModel):
model_config = ConfigDict(
extra='forbid',
)
endpoint: str | None = Field(
None, description='KMS endpoint to Seal/Unseal the key.\n', title='endpoint'
)
class EncryptionKeyNodeID(BaseModel):
model_config = ConfigDict(
extra='forbid',
)
class EncryptionKeyStatic(BaseModel):
model_config = ConfigDict(
extra='forbid',
)
passphrase: str | None = Field(
None, description='Defines the static passphrase value.\n', title='passphrase'
)
class EncryptionKeyTPM(BaseModel):
model_config = ConfigDict(
extra='forbid',
)
checkSecurebootStatusOnEnroll: bool | None = Field(
None,
description='Check that Secureboot is enabled in the EFI firmware.\nIf Secureboot is not enabled, the enrollment of the key will fail. As the TPM key is anyways bound to the value of PCR 7, changing Secureboot status or configuration after the initial enrollment will make the key unusable.\n',
title='checkSecurebootStatusOnEnroll',
)
class Provider(Enum):
luks2 = 'luks2'
class Cipher(Enum):
aes_xts_plain64 = 'aes-xts-plain64'
xchacha12_aes_adiantum_plain64 = 'xchacha12,aes-adiantum-plain64'
xchacha20_aes_adiantum_plain64 = 'xchacha20,aes-adiantum-plain64'
class Options(Enum):
no_read_workqueue = 'no_read_workqueue'
no_write_workqueue = 'no_write_workqueue'
same_cpu_crypt = 'same_cpu_crypt'
class ApiVersion(Enum):
v1alpha1 = 'v1alpha1'
class Kind(Enum):
ExistingVolumeConfig = 'ExistingVolumeConfig'
class Type(Enum):
ext4 = 'ext4'
xfs = 'xfs'
class FilesystemSpec(BaseModel):
model_config = ConfigDict(
extra='forbid',
)
type: Type | None = Field(
None, description='Filesystem type. Default is xfs.\n', title='type'
)
projectQuotaSupport: bool | None = Field(
None,
description='Enables project quota support, valid only for xfs filesystem.\n\nNote: changing this value might require a full remount of the filesystem.\n',
title='projectQuotaSupport',
)
class MountSpec(BaseModel):
model_config = ConfigDict(
extra='forbid',
)
readOnly: bool | None = Field(
None, description='Mount the volume read-only.\n', title='readOnly'
)
class ProvisioningSpec(BaseModel):
model_config = ConfigDict(
extra='forbid',
)
diskSelector: DiskSelector | None = Field(
None, description='The disk selector expression.\n', title='diskSelector'
)
grow: bool | None = Field(
None,
description='Should the volume grow to the size of the disk (if possible).\n',
title='grow',
)
minSize: str | None = Field(
None,
description='The minimum size of the volume.\n\nSize is specified in bytes, but can be expressed in human readable format, e.g. 100MB.\n',
title='minSize',
)
maxSize: str | None = Field(
None,
description='The maximum size of the volume, if not specified the volume can grow to the size of the\ndisk.\n\nSize is specified in bytes, but can be expressed in human readable format, e.g. 100MB.\n',
title='maxSize',
)
class KindModel(Enum):
RawVolumeConfig = 'RawVolumeConfig'
class KindModel1(Enum):
SwapVolumeConfig = 'SwapVolumeConfig'
class KindModel2(Enum):
UserVolumeConfig = 'UserVolumeConfig'
class KindModel3(Enum):
VolumeConfig = 'VolumeConfig'
class VolumeSelector(BaseModel):
model_config = ConfigDict(
extra='forbid',
)
match: str | None = Field(
None,
description='The Common Expression Language (CEL) expression to match the volume.\n',
title='match',
)
class KindModel4(Enum):
ZswapConfig = 'ZswapConfig'
class ZswapConfigV1Alpha1(BaseModel):
model_config = ConfigDict(
extra='forbid',
)
apiVersion: ApiVersion = Field(
...,
description='apiVersion is the API version of the resource.\n',
title='apiVersion',
)
kind: KindModel4 = Field(
..., description='kind is the kind of the resource.\n', title='kind'
)
maxPoolPercent: int | None = Field(
None,
description='The maximum percent of memory that zswap can use.\nThis is a percentage of the total system memory.\nThe value must be between 0 and 100.\n',
title='maxPoolPercent',
)
shrinkerEnabled: bool | None = Field(
None,
description='Enable the shrinker feature: kernel might move\ncold pages from zswap to swap device to free up memory\nfor other use cases.\n',
title='shrinkerEnabled',
)
class EncryptionKey(BaseModel):
model_config = ConfigDict(
extra='forbid',
)
slot: int | None = Field(
None, description='Key slot number for LUKS2 encryption.\n', title='slot'
)
static: EncryptionKeyStatic | None = Field(
None,
description='Key which value is stored in the configuration file.\n',
title='static',
)
nodeID: EncryptionKeyNodeID | None = Field(
None,
description='Deterministically generated key from the node UUID and PartitionLabel.\n',
title='nodeID',
)
kms: EncryptionKeyKMS | None = Field(
None, description='KMS managed encryption key.\n', title='kms'
)
tpm: EncryptionKeyTPM | None = Field(
None, description='Enable TPM based disk encryption.\n', title='tpm'
)
lockToState: bool | None = Field(
None,
description='Lock the disk encryption key to the random salt stored in the STATE partition. This is useful to prevent the volume from being unlocked if STATE partition is compromised or replaced. It is recommended to use this option with TPM disk encryption for non-STATE volumes.\n',
title='lockToState',
)
class EncryptionSpec(BaseModel):
model_config = ConfigDict(
extra='forbid',
)
provider: Provider | None = Field(
None,
description='Encryption provider to use for the encryption.\n',
title='provider',
)
keys: list[EncryptionKey] | None = Field(
None,
description='Defines the encryption keys generation and storage method.\n',
title='keys',
)
cipher: Cipher | None = Field(
None,
description='Cipher to use for the encryption. Depends on the encryption provider.\n',
title='cipher',
)
keySize: int | None = Field(
None, description='Defines the encryption key length.\n', title='keySize'
)
blockSize: int | None = Field(
None, description='Defines the encryption sector size.\n', title='blockSize'
)
options: Options | None = Field(
None,
description='Additional perf parameters for the LUKS2 encryption.\n',
title='options',
)
class RawVolumeConfigV1Alpha1(BaseModel):
model_config = ConfigDict(
extra='forbid',
)
apiVersion: ApiVersion = Field(
...,
description='apiVersion is the API version of the resource.\n',
title='apiVersion',
)
kind: KindModel = Field(
..., description='kind is the kind of the resource.\n', title='kind'
)
name: str | None = Field(
None,
description='Name of the volume.\n\nName might be between 1 and 34 characters long and can only contain:\nlowercase and uppercase ASCII letters, digits, and hyphens.\n',
title='name',
)
provisioning: ProvisioningSpec | None = Field(
None,
description='The provisioning describes how the volume is provisioned.\n',
title='provisioning',
)
encryption: EncryptionSpec | None = Field(
None,
description='The encryption describes how the volume is encrypted.\n',
title='encryption',
)
class SwapVolumeConfigV1Alpha1(BaseModel):
model_config = ConfigDict(
extra='forbid',
)
apiVersion: ApiVersion = Field(
...,
description='apiVersion is the API version of the resource.\n',
title='apiVersion',
)
kind: KindModel1 = Field(
..., description='kind is the kind of the resource.\n', title='kind'
)
name: str | None = Field(
None,
description='Name of the volume.\n\nName might be between 1 and 34 characters long and can only contain:\nlowercase and uppercase ASCII letters, digits, and hyphens.\n',
title='name',
)
provisioning: ProvisioningSpec | None = Field(
None,
description='The provisioning describes how the volume is provisioned.\n',
title='provisioning',
)
encryption: EncryptionSpec | None = Field(
None,
description='The encryption describes how the volume is encrypted.\n',
title='encryption',
)
class UserVolumeConfigV1Alpha1(BaseModel):
model_config = ConfigDict(
extra='forbid',
)
apiVersion: ApiVersion = Field(
...,
description='apiVersion is the API version of the resource.\n',
title='apiVersion',
)
kind: KindModel2 = Field(
..., description='kind is the kind of the resource.\n', title='kind'
)
name: str | None = Field(
None,
description='Name of the volume.\n\nName might be between 1 and 34 characters long and can only contain:\nlowercase and uppercase ASCII letters, digits, and hyphens.\n',
title='name',
)
provisioning: ProvisioningSpec | None = Field(
None,
description='The provisioning describes how the volume is provisioned.\n',
title='provisioning',
)
filesystem: FilesystemSpec | None = Field(
None,
description='The filesystem describes how the volume is formatted.\n',
title='filesystem',
)
encryption: EncryptionSpec | None = Field(
None,
description='The encryption describes how the volume is encrypted.\n',
title='encryption',
)
class VolumeConfigV1Alpha1(BaseModel):
model_config = ConfigDict(
extra='forbid',
)
apiVersion: ApiVersion = Field(
...,
description='apiVersion is the API version of the resource.\n',
title='apiVersion',
)
kind: KindModel3 = Field(
..., description='kind is the kind of the resource.\n', title='kind'
)
name: str | None = Field(None, description='Name of the volume.\n', title='name')
provisioning: ProvisioningSpec | None = Field(
None,
description='The provisioning describes how the volume is provisioned.\n',
title='provisioning',
)
encryption: EncryptionSpec | None = Field(
None,
description='The encryption describes how the volume is encrypted.\n',
title='encryption',
)
class VolumeDiscoverySpec(BaseModel):
model_config = ConfigDict(
extra='forbid',
)
volumeSelector: VolumeSelector | None = Field(
None, description='The volume selector expression.\n', title='volumeSelector'
)
class ExistingVolumeConfigV1Alpha1(BaseModel):
model_config = ConfigDict(
extra='forbid',
)
apiVersion: ApiVersion = Field(
...,
description='apiVersion is the API version of the resource.\n',
title='apiVersion',
)
kind: Kind = Field(
..., description='kind is the kind of the resource.\n', title='kind'
)
name: str | None = Field(
None,
description='Name of the volume.\n\nName can only contain:\nlowercase and uppercase ASCII letters, digits, and hyphens.\n',
title='name',
)
discovery: VolumeDiscoverySpec | None = Field(
None,
description='The discovery describes how to find a volume.\n',
title='discovery',
)
mount: MountSpec | None = Field(
None,
description='The mount describes additional mount options.\n',
title='mount',
)

58
src/models/extensions.py Normal file
View File

@@ -0,0 +1,58 @@
# generated by datamodel-codegen:
# filename: schema.tRo.json
# timestamp: 2026-02-20T04:31:38+00:00
from __future__ import annotations
from enum import Enum
from pydantic import BaseModel, ConfigDict, Field
class ConfigFile(BaseModel):
model_config = ConfigDict(
extra='forbid',
)
content: str | None = Field(
None,
description='The content of the extension service config file.\n',
title='content',
)
mountPath: str | None = Field(
None,
description='The mount path of the extension service config file.\n',
title='mountPath',
)
class ApiVersion(Enum):
v1alpha1 = 'v1alpha1'
class Kind(Enum):
ExtensionServiceConfig = 'ExtensionServiceConfig'
class ServiceConfigV1Alpha1(BaseModel):
model_config = ConfigDict(
extra='forbid',
)
apiVersion: ApiVersion = Field(
...,
description='apiVersion is the API version of the resource.\n',
title='apiVersion',
)
kind: Kind = Field(
..., description='kind is the kind of the resource.\n', title='kind'
)
name: str = Field(..., description='Name of the extension service.\n', title='name')
configFiles: list[ConfigFile] | None = Field(
None,
description='The config files for the extension service.\n',
title='configFiles',
)
environment: list[str] | None = Field(
None,
description='The environment for the extension service.\n',
title='environment',
)

37
src/models/hardware.py Normal file
View File

@@ -0,0 +1,37 @@
# generated by datamodel-codegen:
# filename: schema.tRo.json
# timestamp: 2026-02-20T04:31:38+00:00
from __future__ import annotations
from enum import Enum
from pydantic import BaseModel, ConfigDict, Field
class ApiVersion(Enum):
v1alpha1 = 'v1alpha1'
class Kind(Enum):
PCIDriverRebindConfig = 'PCIDriverRebindConfig'
class PCIDriverRebindConfigV1Alpha1(BaseModel):
model_config = ConfigDict(
extra='forbid',
)
apiVersion: ApiVersion = Field(
...,
description='apiVersion is the API version of the resource.\n',
title='apiVersion',
)
kind: Kind = Field(
..., description='kind is the kind of the resource.\n', title='kind'
)
name: str = Field(..., description='PCI device id\n', title='name')
targetDriver: str = Field(
...,
description='Target driver to rebind the PCI device to.\n',
title='targetDriver',
)

220
src/models/network.py Normal file
View File

@@ -0,0 +1,220 @@
# generated by datamodel-codegen:
# filename: schema.tRo.json
# timestamp: 2026-02-20T04:31:38+00:00
from __future__ import annotations
from enum import Enum
from pydantic import BaseModel, ConfigDict, Field, constr
class ApiVersion(Enum):
v1alpha1 = 'v1alpha1'
class Kind(Enum):
NetworkDefaultActionConfig = 'NetworkDefaultActionConfig'
class Ingress(Enum):
accept = 'accept'
block = 'block'
class DefaultActionConfigV1Alpha1(BaseModel):
model_config = ConfigDict(
extra='forbid',
)
apiVersion: ApiVersion = Field(
...,
description='apiVersion is the API version of the resource.\n',
title='apiVersion',
)
kind: Kind = Field(
..., description='kind is the kind of the resource.\n', title='kind'
)
ingress: Ingress | None = Field(
None,
description='Default action for all not explicitly configured ingress traffic: accept or block.\n',
title='ingress',
)
class EthernetChannelsConfig(BaseModel):
model_config = ConfigDict(
extra='forbid',
)
rx: int | None = Field(None, description='Number of RX channels.\n', title='rx')
tx: int | None = Field(None, description='Number of TX channels.\n', title='tx')
other: int | None = Field(
None, description='Number of other channels.\n', title='other'
)
combined: int | None = Field(
None, description='Number of combined channels.\n', title='combined'
)
class KindModel(Enum):
EthernetConfig = 'EthernetConfig'
class EthernetRingsConfig(BaseModel):
model_config = ConfigDict(
extra='forbid',
)
rx: int | None = Field(None, description='Number of RX rings.\n', title='rx')
tx: int | None = Field(None, description='Number of TX rings.\n', title='tx')
rx_mini: int | None = Field(
None, alias='rx-mini', description='Number of RX mini rings.\n', title='rx-mini'
)
rx_jumbo: int | None = Field(
None,
alias='rx-jumbo',
description='Number of RX jumbo rings.\n',
title='rx-jumbo',
)
rx_buf_len: int | None = Field(
None, alias='rx-buf-len', description='RX buffer length.\n', title='rx-buf-len'
)
cqe_size: int | None = Field(
None, alias='cqe-size', description='CQE size.\n', title='cqe-size'
)
tx_push: bool | None = Field(
None, alias='tx-push', description='TX push enabled.\n', title='tx-push'
)
rx_push: bool | None = Field(
None, alias='rx-push', description='RX push enabled.\n', title='rx-push'
)
tx_push_buf_len: int | None = Field(
None,
alias='tx-push-buf-len',
description='TX push buffer length.\n',
title='tx-push-buf-len',
)
tcp_data_split: bool | None = Field(
None,
alias='tcp-data-split',
description='TCP data split enabled.\n',
title='tcp-data-split',
)
class IngressRule(BaseModel):
model_config = ConfigDict(
extra='forbid',
)
subnet: constr(pattern=r'^[0-9a-f.:]+/\d{1,3}$') | None = Field(
None, description='Subnet defines a source subnet.\n', title='subnet'
)
except_: constr(pattern=r'^[0-9a-f.:]+/\d{1,3}$') | None = Field(
None,
alias='except',
description='Except defines a source subnet to exclude from the rule, it gets excluded from the subnet.\n',
title='except',
)
class KindModel1(Enum):
KubeSpanEndpoints = 'KubeSpanEndpoints'
class KubespanEndpointsConfigV1Alpha1(BaseModel):
model_config = ConfigDict(
extra='forbid',
)
apiVersion: ApiVersion = Field(
...,
description='apiVersion is the API version of the resource.\n',
title='apiVersion',
)
kind: KindModel1 = Field(
..., description='kind is the kind of the resource.\n', title='kind'
)
extraAnnouncedEndpoints: list[str] | None = Field(
None,
description='A list of extra Wireguard endpoints to announce from this machine.\n\nTalos automatically adds endpoints based on machine addresses, public IP, etc.\nThis field allows to add extra endpoints which are managed outside of Talos, e.g. NAT mapping.\n',
title='extraAnnouncedEndpoints',
)
class KindModel2(Enum):
NetworkRuleConfig = 'NetworkRuleConfig'
class Protocol(Enum):
tcp = 'tcp'
udp = 'udp'
icmp = 'icmp'
icmpv6 = 'icmpv6'
class RulePortSelector(BaseModel):
model_config = ConfigDict(
extra='forbid',
)
ports: list[int | str] | None = Field(
None,
description='Ports defines a list of port ranges or single ports.\nThe port ranges are inclusive, and should not overlap.\n',
title='ports',
)
protocol: Protocol | None = Field(
None,
description='Protocol defines traffic protocol (e.g. TCP or UDP).\n',
title='protocol',
)
class EthernetConfigV1Alpha1(BaseModel):
model_config = ConfigDict(
extra='forbid',
)
apiVersion: ApiVersion = Field(
...,
description='apiVersion is the API version of the resource.\n',
title='apiVersion',
)
kind: KindModel = Field(
..., description='kind is the kind of the resource.\n', title='kind'
)
name: str = Field(..., description='Name of the link (interface).\n', title='name')
features: dict[constr(pattern=r'.*'), bool] | None = Field(
None,
description='Configuration for Ethernet features.\n\nSet of features available and whether they can be enabled or disabled is driver specific.\nUse talosctl get ethernetstatus <link> -o yaml to get the list of available features and\ntheir current status.\n',
title='features',
)
rings: EthernetRingsConfig | None = Field(
None,
description='Configuration for Ethernet link rings.\n\nThis is similar to ethtool -G command.\n',
title='rings',
)
channels: EthernetChannelsConfig | None = Field(
None,
description='Configuration for Ethernet link channels.\n\nThis is similar to ethtool -L command.\n',
title='channels',
)
class RuleConfigV1Alpha1(BaseModel):
model_config = ConfigDict(
extra='forbid',
)
apiVersion: ApiVersion = Field(
...,
description='apiVersion is the API version of the resource.\n',
title='apiVersion',
)
kind: KindModel2 = Field(
..., description='kind is the kind of the resource.\n', title='kind'
)
name: str = Field(..., description='Name of the config document.\n', title='name')
portSelector: RulePortSelector | None = Field(
None,
description='Port selector defines which ports and protocols on the host are affected by the rule.\n',
title='portSelector',
)
ingress: list[IngressRule] | None = Field(
None,
description='Ingress defines which source subnets are allowed to access the host ports/protocols defined by the portSelector.\n',
title='ingress',
)

90
src/models/runtime.py Normal file
View File

@@ -0,0 +1,90 @@
# generated by datamodel-codegen:
# filename: schema.tRo.json
# timestamp: 2026-02-20T04:31:38+00:00
from __future__ import annotations
from enum import Enum
from pydantic import BaseModel, ConfigDict, Field, constr
class ApiVersion(Enum):
v1alpha1 = 'v1alpha1'
class Kind(Enum):
EventSinkConfig = 'EventSinkConfig'
class EventSinkV1Alpha1(BaseModel):
model_config = ConfigDict(
extra='forbid',
)
apiVersion: ApiVersion = Field(
...,
description='apiVersion is the API version of the resource.\n',
title='apiVersion',
)
kind: Kind = Field(
..., description='kind is the kind of the resource.\n', title='kind'
)
endpoint: str | None = Field(
None,
description='The endpoint for the event sink as host:port.\n',
title='endpoint',
)
class KindModel(Enum):
KmsgLogConfig = 'KmsgLogConfig'
class KmsgLogV1Alpha1(BaseModel):
model_config = ConfigDict(
extra='forbid',
)
apiVersion: ApiVersion = Field(
...,
description='apiVersion is the API version of the resource.\n',
title='apiVersion',
)
kind: KindModel = Field(
..., description='kind is the kind of the resource.\n', title='kind'
)
name: str | None = Field(
None, description='Name of the config document.\n', title='name'
)
url: constr(pattern=r'^(tcp|udp)://') | None = Field(
None,
description='The URL encodes the log destination.\nThe scheme must be tcp:// or udp://.\nThe path must be empty.\nThe port is required.\n',
title='url',
)
class KindModel1(Enum):
WatchdogTimerConfig = 'WatchdogTimerConfig'
class WatchdogTimerV1Alpha1(BaseModel):
model_config = ConfigDict(
extra='forbid',
)
apiVersion: ApiVersion = Field(
...,
description='apiVersion is the API version of the resource.\n',
title='apiVersion',
)
kind: KindModel1 = Field(
..., description='kind is the kind of the resource.\n', title='kind'
)
device: str | None = Field(
None, description='Path to the watchdog device.\n', title='device'
)
timeout: (
constr(pattern=r'^[-+]?(((\d+(\.\d*)?|\d*(\.\d+)+)([nuµm]?s|m|h))|0)+$') | None
) = Field(
None,
description='Timeout for the watchdog.\n\nIf Talos is unresponsive for this duration, the watchdog will reset the system.\n\nDefault value is 1 minute, minimum value is 10 seconds.\n',
title='timeout',
)

37
src/models/security.py Normal file
View File

@@ -0,0 +1,37 @@
# generated by datamodel-codegen:
# filename: schema.tRo.json
# timestamp: 2026-02-20T04:31:38+00:00
from __future__ import annotations
from enum import Enum
from pydantic import BaseModel, ConfigDict, Field
class ApiVersion(Enum):
v1alpha1 = 'v1alpha1'
class Kind(Enum):
TrustedRootsConfig = 'TrustedRootsConfig'
class TrustedRootsConfigV1Alpha1(BaseModel):
model_config = ConfigDict(
extra='forbid',
)
apiVersion: ApiVersion = Field(
...,
description='apiVersion is the API version of the resource.\n',
title='apiVersion',
)
kind: Kind = Field(
..., description='kind is the kind of the resource.\n', title='kind'
)
name: str = Field(..., description='Name of the config document.\n', title='name')
certificates: str | None = Field(
None,
description='List of additional trusted certificate authorities (as PEM-encoded certificates).\n\nMultiple certificates can be provided in a single config document, separated by newline characters.\n',
title='certificates',
)

39
src/models/siderolink.py Normal file
View File

@@ -0,0 +1,39 @@
# generated by datamodel-codegen:
# filename: schema.tRo.json
# timestamp: 2026-02-20T04:31:38+00:00
from __future__ import annotations
from enum import Enum
from pydantic import BaseModel, ConfigDict, Field, constr
class ApiVersion(Enum):
v1alpha1 = 'v1alpha1'
class Kind(Enum):
SideroLinkConfig = 'SideroLinkConfig'
class ConfigV1Alpha1(BaseModel):
model_config = ConfigDict(
extra='forbid',
)
apiVersion: ApiVersion = Field(
...,
description='apiVersion is the API version of the resource.\n',
title='apiVersion',
)
kind: Kind = Field(
..., description='kind is the kind of the resource.\n', title='kind'
)
apiUrl: constr(pattern=r'^(https|grpc)://') | None = Field(
None, description='SideroLink API URL to connect to.\n', title='apiUrl'
)
uniqueToken: str | None = Field(
None,
description='SideroLink unique token to use for the connection (optional).\n\nThis value is overridden with META key UniqueMachineToken.\n',
title='uniqueToken',
)

1873
src/models/v1alpha1.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -12,10 +12,10 @@ talosctl gen config {{ node.cluster.name }} https://{{ node.cluster.controlPlane
--install-image factory.talos.dev/metal-installer/{{ node.schematicId }}:v{{ node.talosVersion }} \ --install-image factory.talos.dev/metal-installer/{{ node.schematicId }}:v{{ node.talosVersion }} \
{% for patch in node.patches -%} {% for patch in node.patches -%}
{# The double call to tojson is needed to properly escape the patch (object -> json -> string) -#} {# The double call to tojson is needed to properly escape the patch (object -> json -> string) -#}
--config-patch {{ patch|tojson|tojson }} \ --config-patch {{ helper.model_dump_json(patch)|tojson }} \
{% endfor -%} {% endfor -%}
{% for patch in node.patchesControlPlane -%} {% for patch in node.patchesControlPlane -%}
--config-patch-control-plane {{ patch|tojson|tojson }} \ --config-patch-control-plane {{ helper.model_dump_json(patch)|tojson }} \
{% endfor -%} {% endfor -%}
--with-docs=false \ --with-docs=false \
--with-examples=false \ --with-examples=false \

View File

@@ -5,9 +5,9 @@
import base64 import base64
import functools import functools
import json
import pathlib import pathlib
import sys import sys
from typing import Annotated, Any, List, Literal
import git import git
import requests import requests
@@ -15,6 +15,17 @@ import yaml
from jinja2 import Environment, FileSystemLoader, StrictUndefined, Template from jinja2 import Environment, FileSystemLoader, StrictUndefined, Template
from mergedeep import Strategy, merge from mergedeep import Strategy, merge
from netaddr import IPAddress from netaddr import IPAddress
from pydantic import (
BaseModel,
BeforeValidator,
ConfigDict,
HttpUrl,
IPvAnyAddress,
ValidationInfo,
)
from pydantic_extra_types.semantic_version import SemanticVersion
from models import Model as TalosModel
REPO = git.Repo(sys.path[0], search_parent_directories=True) REPO = git.Repo(sys.path[0], search_parent_directories=True)
assert REPO.working_dir is not None assert REPO.working_dir is not None
@@ -39,6 +50,38 @@ TEMPLATES = Environment(
) )
class ServerConfig(BaseModel):
model_config = ConfigDict(strict=True, extra="forbid")
tftpIp: IPvAnyAddress
httpUrl: HttpUrl
class TailscaleConfig(BaseModel):
model_config = ConfigDict(strict=True, extra="forbid")
loginServer: HttpUrl
authKey: str
class Config(BaseModel):
model_config = ConfigDict(strict=True, extra="forbid")
server: ServerConfig
tailscale: TailscaleConfig
class Cluster(BaseModel):
model_config = ConfigDict(strict=True, extra="forbid")
name: str
production: bool
controlPlaneIp: IPvAnyAddress
# TODO: Path
secretsFile: str
sopsKeyFile: str
# When we try to make a deep copy of the nodes dict it fails as the Template # When we try to make a deep copy of the nodes dict it fails as the Template
# does not implement __deepcopy__, so this wrapper type facilitates that # does not implement __deepcopy__, so this wrapper type facilitates that
class TemplateWrapper: class TemplateWrapper:
@@ -51,31 +94,68 @@ class TemplateWrapper:
return self return self
def render_templates(node: dict, args: dict): def render_patch(wrapper: Any, info: ValidationInfo):
class Inner(json.JSONEncoder): if not isinstance(wrapper, TemplateWrapper):
def default(self, o): raise RuntimeError("Expected TemplateWrapper")
if isinstance(o, TemplateWrapper): args = (info.context or {}) | {"node": info.data}
try: try:
rendered = o.template.render(args | {"node": node}) rendered = wrapper.template.render(args)
except Exception as e: except Exception as e:
e.add_note(f"While rendering for: {node['hostname']}") e.add_note(f"While rendering for: {args['node']['hostname']}")
raise e raise e
# Parse the rendered yaml # Parse the rendered yaml
return yaml.safe_load(rendered) return yaml.safe_load(rendered)
return super().default(o)
return Inner class Node(BaseModel):
model_config = ConfigDict(strict=True, extra="forbid")
schematicId: str
arch: Literal["amd64"]
talosVersion: SemanticVersion
kubernetesVersion: SemanticVersion
kernelArgs: List[str]
extraKernelArgs: List[str]
dns: List[IPvAnyAddress]
# TODO: Validation
ntp: str
install: bool
advertiseRoutes: bool
serial: str
interface: str
ip: IPvAnyAddress
netmask: IPvAnyAddress
gateway: IPvAnyAddress
# TODO: Extra validation
installDisk: str
autoInstall: bool
cluster: Cluster
hostname: str
filename: str
type: Literal["controlplane", "worker"]
patches: List[Annotated[TalosModel, BeforeValidator(render_patch)]]
patchesControlPlane: List[Annotated[TalosModel, BeforeValidator(render_patch)]]
def tailscale_subnet(gateway: str, netmask: str): def tailscale_subnet(gateway: IPvAnyAddress, netmask: IPvAnyAddress):
netmask_bits = IPAddress(netmask).netmask_bits() netmask_bits = IPAddress(netmask.exploded).netmask_bits()
return f"{IPAddress(gateway) & IPAddress(netmask)}/{netmask_bits}" return f"{IPAddress(gateway.exploded) & IPAddress(netmask.exploded)}/{netmask_bits}"
def load_secret(path: str): def load_secret(path: str):
with open(path) as f: with open(path) as f:
return base64.b64encode(f.read().encode()).decode() return base64.b64encode(f.read().encode()).decode()
def model_dump_json(model: BaseModel):
return model.model_dump_json(exclude_none=True)
@functools.cache @functools.cache
def get_schematic_id(schematic: str): def get_schematic_id(schematic: str):
"""Lookup the schematic id associated with a given schematic""" """Lookup the schematic id associated with a given schematic"""
@@ -166,13 +246,19 @@ def main():
with open(ROOT.joinpath("secrets.yaml")) as fyaml: with open(ROOT.joinpath("secrets.yaml")) as fyaml:
merge(config, yaml.safe_load(fyaml), strategy=Strategy.TYPESAFE_REPLACE) merge(config, yaml.safe_load(fyaml), strategy=Strategy.TYPESAFE_REPLACE)
config = Config(**config)
template_args = { template_args = {
"config": config, "config": config,
"root": ROOT, "root": ROOT,
"helper": {"tailscale_subnet": tailscale_subnet, "load_secret": load_secret}, "helper": {
"tailscale_subnet": tailscale_subnet,
"load_secret": load_secret,
"model_dump_json": model_dump_json,
},
} }
nodes = [] nodes: List[Node] = []
for fullname in walk_files(NODES): for fullname in walk_files(NODES):
filename = str(fullname.relative_to(NODES).parent) + "/" + fullname.stem filename = str(fullname.relative_to(NODES).parent) + "/" + fullname.stem
@@ -186,24 +272,15 @@ def main():
) )
yml_data["hostname"] = fullname.stem yml_data["hostname"] = fullname.stem
yml_data["filename"] = filename yml_data["filename"] = filename
nodes.append(yml_data) node = Node.model_validate(yml_data, context=template_args)
nodes.append(node)
# Quick and dirty way to resolve all the templates using a custom encoder
nodes = list(
map(
lambda node: json.loads(
json.dumps(node, cls=render_templates(node, template_args))
),
nodes,
)
)
# HACK: We can't hash a dict, so we first convert it to json, the use set # HACK: We can't hash a dict, so we first convert it to json, the use set
# to get all the unique entries, and then convert it back # to get all the unique entries, and then convert it back
# NOTE: This assumes that all nodes in the cluster use the same definition for the cluster # NOTE: This assumes that all nodes in the cluster use the same definition for the cluster
clusters = list( clusters = list(
json.loads(cluster) Cluster.model_validate_json(cluster)
for cluster in set(json.dumps(node["cluster"]) for node in nodes) for cluster in set(node.cluster.model_dump_json() for node in nodes)
) )
template_args |= {"nodes": nodes, "clusters": clusters} template_args |= {"nodes": nodes, "clusters": clusters}

15
tools/update_models Executable file
View File

@@ -0,0 +1,15 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT=$(git rev-parse --show-toplevel)
MODELS_DIR=${ROOT}/src/models
rm -rf ${MODELS_DIR}
SCHEMA_FILE=$(mktemp schema.XXX.json)
function cleanup() {
rm -rf ${SCHEMA_FILE}
}
trap cleanup EXIT
curl https://raw.githubusercontent.com/siderolabs/talos/refs/heads/release-1.11/website/content/v1.11/schemas/config.schema.json > ${SCHEMA_FILE}
uvx --from datamodel-code-generator datamodel-codegen --input ${SCHEMA_FILE} --input-file-type jsonschema --output ${MODELS_DIR} --output-model pydantic_v2.BaseModel

126
uv.lock generated
View File

@@ -2,6 +2,15 @@ version = 1
revision = 3 revision = 3
requires-python = ">=3.13" requires-python = ">=3.13"
[[package]]
name = "annotated-types"
version = "0.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
]
[[package]] [[package]]
name = "bootstrap" name = "bootstrap"
version = "0.1.0" version = "0.1.0"
@@ -11,8 +20,11 @@ dependencies = [
{ name = "jinja2" }, { name = "jinja2" },
{ name = "mergedeep" }, { name = "mergedeep" },
{ name = "netaddr" }, { name = "netaddr" },
{ name = "pydantic" },
{ name = "pydantic-extra-types" },
{ name = "pyyaml" }, { name = "pyyaml" },
{ name = "requests" }, { name = "requests" },
{ name = "semver" },
] ]
[package.metadata] [package.metadata]
@@ -21,8 +33,11 @@ requires-dist = [
{ name = "jinja2", specifier = "==3.1.6" }, { name = "jinja2", specifier = "==3.1.6" },
{ name = "mergedeep", specifier = "==1.3.4" }, { name = "mergedeep", specifier = "==1.3.4" },
{ name = "netaddr", specifier = "==1.3.0" }, { name = "netaddr", specifier = "==1.3.0" },
{ name = "pydantic", specifier = ">=2.12.5" },
{ name = "pydantic-extra-types", specifier = ">=2.11.0" },
{ name = "pyyaml", specifier = "==6.0.3" }, { name = "pyyaml", specifier = "==6.0.3" },
{ name = "requests", specifier = "==2.32.5" }, { name = "requests", specifier = "==2.32.5" },
{ name = "semver", specifier = ">=3.0.4" },
] ]
[[package]] [[package]]
@@ -190,6 +205,87 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/12/cc/f4fe2c7ce68b92cbf5b2d379ca366e1edae38cccaad00f69f529b460c3ef/netaddr-1.3.0-py3-none-any.whl", hash = "sha256:c2c6a8ebe5554ce33b7d5b3a306b71bbb373e000bbbf2350dd5213cc56e3dbbe", size = 2262023, upload-time = "2024-05-28T21:30:34.191Z" }, { url = "https://files.pythonhosted.org/packages/12/cc/f4fe2c7ce68b92cbf5b2d379ca366e1edae38cccaad00f69f529b460c3ef/netaddr-1.3.0-py3-none-any.whl", hash = "sha256:c2c6a8ebe5554ce33b7d5b3a306b71bbb373e000bbbf2350dd5213cc56e3dbbe", size = 2262023, upload-time = "2024-05-28T21:30:34.191Z" },
] ]
[[package]]
name = "pydantic"
version = "2.12.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-types" },
{ name = "pydantic-core" },
{ name = "typing-extensions" },
{ name = "typing-inspection" },
]
sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" },
]
[[package]]
name = "pydantic-core"
version = "2.41.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" },
{ url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" },
{ url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" },
{ url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" },
{ url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" },
{ url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" },
{ url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" },
{ url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" },
{ url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" },
{ url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" },
{ url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" },
{ url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" },
{ url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" },
{ url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" },
{ url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" },
{ url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" },
{ url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" },
{ url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" },
{ url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" },
{ url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" },
{ url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" },
{ url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" },
{ url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" },
{ url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" },
{ url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" },
{ url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" },
{ url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" },
{ url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" },
{ url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" },
{ url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" },
{ url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" },
{ url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" },
{ url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" },
{ url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" },
{ url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" },
{ url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" },
{ url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" },
{ url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" },
{ url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" },
{ url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" },
{ url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" },
{ url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" },
]
[[package]]
name = "pydantic-extra-types"
version = "2.11.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pydantic" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/fd/35/2fee58b1316a73e025728583d3b1447218a97e621933fc776fb8c0f2ebdd/pydantic_extra_types-2.11.0.tar.gz", hash = "sha256:4e9991959d045b75feb775683437a97991d02c138e00b59176571db9ce634f0e", size = 157226, upload-time = "2025-12-31T16:18:27.944Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fe/17/fabd56da47096d240dd45ba627bead0333b0cf0ee8ada9bec579287dadf3/pydantic_extra_types-2.11.0-py3-none-any.whl", hash = "sha256:84b864d250a0fc62535b7ec591e36f2c5b4d1325fa0017eb8cda9aeb63b374a6", size = 74296, upload-time = "2025-12-31T16:18:26.38Z" },
]
[[package]] [[package]]
name = "pyyaml" name = "pyyaml"
version = "6.0.3" version = "6.0.3"
@@ -241,6 +337,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
] ]
[[package]]
name = "semver"
version = "3.0.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/d1/d3159231aec234a59dd7d601e9dd9fe96f3afff15efd33c1070019b26132/semver-3.0.4.tar.gz", hash = "sha256:afc7d8c584a5ed0a11033af086e8af226a9c0b206f313e0301f8dd7b6b589602", size = 269730, upload-time = "2025-01-24T13:19:27.617Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a6/24/4d91e05817e92e3a61c8a21e08fd0f390f5301f1c448b137c57c4bc6e543/semver-3.0.4-py3-none-any.whl", hash = "sha256:9c824d87ba7f7ab4a1890799cec8596f15c1241cb473404ea1cb0c55e4b04746", size = 17912, upload-time = "2025-01-24T13:19:24.949Z" },
]
[[package]] [[package]]
name = "smmap" name = "smmap"
version = "5.0.2" version = "5.0.2"
@@ -250,6 +355,27 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303, upload-time = "2025-01-02T07:14:38.724Z" }, { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303, upload-time = "2025-01-02T07:14:38.724Z" },
] ]
[[package]]
name = "typing-extensions"
version = "4.15.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
]
[[package]]
name = "typing-inspection"
version = "0.4.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
]
[[package]] [[package]]
name = "urllib3" name = "urllib3"
version = "2.5.0" version = "2.5.0"