diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dcb65aad..e4984143 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,6 +22,8 @@ jobs: - run: make install - run: make lint + # python_anvil/types must be regenerated when schema/ changes + - run: make check-generated test: name: Test diff --git a/.github/workflows/sync-api-types.yml b/.github/workflows/sync-api-types.yml new file mode 100644 index 00000000..6d9c0158 --- /dev/null +++ b/.github/workflows/sync-api-types.yml @@ -0,0 +1,158 @@ +name: Sync API Types + +# Refreshes schema/ from the live Anvil API and the published @anvilco/types, +# regenerates python_anvil/types, and opens one PR when any of it changed. +# Runs after a production rollout completes: Foundry's postsync step sends +# `anvil-production-deploy` to every repo in its --webhook-repos list +# (anvil-infra: k8s/anvil/base/foundry/postsync.yaml). That dispatch is +# best-effort with no retry, so the weekly cron is a backstop for a dropped one. +# anvil-js's sync-api-types.yml does the same for @anvilco/types. + +on: + repository_dispatch: + types: [anvil-production-deploy] + schedule: + - cron: '0 10 * * 1' # Every Monday at 10am UTC, after anvil-js's 9am sync + workflow_dispatch: + +# BRANCH is force-pushed, so never let two runs race on it. +concurrency: sync-api-types + +env: + BRANCH: chore/sync-api-types + SCHEMA_DIR: schema + TYPES_DIR: python_anvil/types + +jobs: + sync-api-types: + name: Sync API Types + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + contents: read + steps: + # Always master, so a manual run from another branch can't put that + # branch's commits in the PR. No persisted credentials: CI_GITHUB_TOKEN + # is only handed to git and gh in the steps that push, not to the + # install and the generators. + - uses: actions/checkout@v7 + with: + ref: master + persist-credentials: false + + - run: pipx install poetry + - name: Set up Python + uses: actions/setup-python@v7 + with: + python-version-file: '.python-version' + cache: 'poetry' + + - run: make install + + # One step, so the types are never regenerated from a spec that failed + # to fetch, and a failed generation is discarded along with the specs. + - name: Fetch the specs and generate the types + id: generate + continue-on-error: true + run: | + make update-schema + make generate + + - name: Discard the output if that failed + if: steps.generate.outcome != 'success' + run: git checkout -- "$SCHEMA_DIR" "$TYPES_DIR" + + - name: Check for changes + id: changes + run: | + echo "changed=$(git diff --quiet -- "$SCHEMA_DIR" "$TYPES_DIR" && echo false || echo true)" >> "$GITHUB_OUTPUT" + git diff --stat + + - name: Open or update PR + if: steps.changes.outputs.changed == 'true' + env: + GH_TOKEN: ${{ secrets.CI_GITHUB_TOKEN }} + EVENT_NAME: ${{ github.event_name }} + DEPLOY_VERSION: ${{ github.event.client_payload.version }} + DEPLOY_PR_URL: ${{ github.event.client_payload.pullRequest.url }} + DEPLOYED_AT: ${{ github.event.client_payload.deployedAt }} + run: | + # Not GITHUB_TOKEN: pushes and PRs made with it don't trigger + # workflows, so the PR would never get its CI checks. + git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" + + # A deploy that didn't move the API again would otherwise force-push + # an identical commit, re-running CI and discarding any edits made + # on the branch. + PR=$(gh pr list --head "$BRANCH" --state open --json number --jq '.[0].number // empty') + if [ -n "$PR" ] && git fetch -q --depth=1 origin "$BRANCH" && git diff --quiet FETCH_HEAD -- "$SCHEMA_DIR" "$TYPES_DIR"; then + echo "PR #$PR already has this output" + exit 0 + fi + + # Note the sync under Unreleased in the changelog, once per release + python3 - <<'PY' + import re + from pathlib import Path + + path = Path("CHANGELOG.md") + text = path.read_text(encoding="utf-8") + note = "- Sync the generated API types in `python_anvil.types` with the production API." + match = re.search(r"^## \[Unreleased\]\n(?P.*?)(?=^## \[|\Z)", text, re.M | re.S) + if match and note not in match["body"]: + body = match["body"].rstrip("\n") + section = f"{body}\n{note}\n\n" if body else f"\n{note}\n\n" + text = text[: match.start("body")] + section + text[match.end("body") :] + path.write_text(text, encoding="utf-8") + PY + + if [ "$EVENT_NAME" = repository_dispatch ]; then + TRIGGER="Anvil production deploy \`$DEPLOY_VERSION\` ($DEPLOY_PR_URL), deployed $DEPLOYED_AT." + else + TRIGGER="\`$EVENT_NAME\` run." + fi + { + echo "Automated sync of the generated types from the production Anvil API. Triggered by $TRIGGER" + echo + echo "- \`$SCHEMA_DIR/\`: the GraphQL schema (from the published \`@anvilco/types\`) and the OpenAPI spec" + echo "- \`$TYPES_DIR/\`: regenerated from them with \`make generate\`" + echo + echo "If a type or field was removed or renamed, note it under Unreleased in CHANGELOG.md as a breaking change before merging." + echo + echo "A later deploy that changes these files again rebuilds this branch from \`master\`, replacing any edits made here." + } > "$RUNNER_TEMP/pr-body.md" + + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git checkout -B "$BRANCH" + git add -A -- "$SCHEMA_DIR" "$TYPES_DIR" CHANGELOG.md + git commit -m "Sync the generated API types from production" + git push --force origin "$BRANCH" + + if [ -n "$PR" ]; then + gh pr edit "$PR" --body-file "$RUNNER_TEMP/pr-body.md" + else + gh pr create \ + --title "Sync the generated API types from production" \ + --body-file "$RUNNER_TEMP/pr-body.md" \ + --base master \ + --head "$BRANCH" + fi + + # Production moved back to match master (e.g. a rollback before the PR + # merged), so an open sync PR is now wrong. + - name: Close stale PR + if: steps.changes.outputs.changed == 'false' && steps.generate.outcome == 'success' + env: + GH_TOKEN: ${{ secrets.CI_GITHUB_TOKEN }} + run: | + PR=$(gh pr list --head "$BRANCH" --state open --json number --jq '.[0].number // empty') + if [ -n "$PR" ]; then + gh pr close "$PR" --delete-branch --comment "Production now matches master, so this sync is no longer needed." + fi + + - name: Fail if the generator failed + if: steps.generate.outcome != 'success' + run: | + echo "::error::Fetching the specs or generating the types failed. See the step above." + exit 1 diff --git a/CHANGELOG.md b/CHANGELOG.md index d7295d32..8bfd5d6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,8 +30,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The `createEtchPacket` mutation gains `organizationEid` and `allowUpdates`, and the mutations and default response fields now match node-anvil's exactly. `forgeSubmit`'s default fields drop `resolvedPayload` and `weldData.status`, and gain `continueURL` and `payloadValue`; pass `response_query` to select others. - `forge_submit` with a `webhook_url` works. `ForgeSubmitPayload.webhook_url` was a pydantic `HttpUrl`, which the JSON encoder rejected, so it failed before sending; it is a `str` now. - `fill_pdf` sends `version_number=0` (it used to drop it), and `generate_etch_signing_url` is also available as `generate_etch_sign_url`. +- `Anvil()` with no key reads `ANVIL_API_KEY` from the environment, the way the examples and the CLI do. The client keeps `api_key` and `access_token` as attributes. +- Every method takes a `timeout` keyword argument, overriding the client's timeout for that request. +- `create_etch_packet` and `forge_submit` take the payload as a JSON string too, like `fill_pdf` and `generate_pdf`, and their dicts may hold any kind of file (a template reference, Markdown, HTML or an upload) or model instances, with snake_case or camelCase keys. `query` and `mutate` take `variables` as a JSON string. A `response_query` may be given with or without braces. An `in-person` signer is accepted. An uploaded open file is sent under its own filename rather than `document.pdf`. +- `GeneratePDFPayload` extends the generated request type like the other payload models, so `page`, `logo`, `include_timestamp` and `output_format` are typed. `utils.camel_to_snake` keeps acronyms whole now (`webhookURL` -> `webhook_url`; it gave `webhook_u_r_l`). The CLI's `cast` and `weld` without an eid exit with a usage error (code 2) instead of a traceback. `CREATE_ETCH_PACKET` and `FORGE_SUBMIT` are still exported from the builder modules; a `BaseQuery` subclass with its own `mutation` template still works. - The client's queries and mutations are checked against the API schema in CI, and `make update-schema` refreshes the schema from the published `@anvilco/types` package. - Add `test-client.py`, which runs every method against a real environment: `ANVIL_API_KEY=... make test-client`. +- Add `python_anvil.types`: pydantic models generated from the API's OpenAPI spec and GraphQL schema, the Python counterpart of `@anvilco/types`. `types.rest` has the REST request bodies, `types.graphql` every type in the schema, and `types.operations` the arguments of every query and mutation, like `CreateEtchPacketArgs`. The client's payload models extend them, so `FillPDFPayload`, `EtchSigner`, `CreateEtchPacketPayload` and `ForgeSubmitPayload` now accept every option the API has. BREAKING: they also validate those options the way the API documents them: an enum value the API doesn't accept (like `signature_mode="wave"`), or a number out of its documented range (like `token_valid_for_minutes` above a day), is a `ValidationError` before anything is sent, where 5.x passed it through for the API to reject. `font_size` and `routing_order` keep accepting any value, as they did. `ForgeSubmitPayload.webhook_url` is a plain `str` now, not an `HttpUrl`. +- A Sync API Types workflow refreshes `schema/` and regenerates `python_anvil/types` after each production deploy, opening a PR when anything changed. `make update-schema generate` does the same by hand. ## [v5.0.3] - 2025-02-24 diff --git a/Makefile b/Makefile index 624c9d34..cf0a26dd 100644 --- a/Makefile +++ b/Makefile @@ -30,9 +30,17 @@ test-client: install ## Run every method against a real environment (staging by poetry run python test-client.py .PHONY: update-schema -update-schema: install ## Refresh schema/anvil_schema.graphql from the published @anvilco/types +update-schema: install ## Refresh schema/ from the published @anvilco/types and the API's OpenAPI spec poetry run python dev/fetch_schema.py +.PHONY: generate +generate: install ## Regenerate python_anvil/types from schema/ + poetry run python dev/generate_types.py + +.PHONY: check-generated +check-generated: generate ## Fail if python_anvil/types is out of date with schema/ + git diff --exit-code -- python_anvil/types + .PHONY: build build: install ## Build the sdist and wheel into dist/ rm -rf dist diff --git a/dev/fetch_schema.py b/dev/fetch_schema.py index 06d13e6f..2e07ed0a 100644 --- a/dev/fetch_schema.py +++ b/dev/fetch_schema.py @@ -1,15 +1,20 @@ #!/usr/bin/env python3 -"""Refreshes schema/anvil_schema.graphql from the published @anvilco/types package. +"""Refreshes the API specs the generated types come from. -@anvilco/types (https://www.npmjs.com/package/@anvilco/types) ships the Anvil -GraphQL schema, regenerated from production after every deploy. node-anvil -validates its operations against that copy, and so does this client: using -the same source keeps the two clients in step, and needs no API key. +- schema/anvil_schema.graphql, from the published @anvilco/types npm package + (https://www.npmjs.com/package/@anvilco/types). It ships the Anvil GraphQL + schema, regenerated from production after every deploy. node-anvil + validates its operations against that copy, and so does this client: + using the same source keeps the two clients in step. +- schema/openapi.json, the API's own OpenAPI spec, from production. + +Neither needs an API key. Then run `dev/generate_types.py` (`make generate`) +to regenerate python_anvil/types from them. Usage: - poetry run python dev/fetch_schema.py [version] + poetry run python dev/fetch_schema.py [@anvilco/types version] -Defaults to the latest version. On any failure the existing file is untouched. +Defaults to the latest version. On any failure the existing files are untouched. """ from __future__ import annotations @@ -25,24 +30,31 @@ PACKAGE = "@anvilco/types" REGISTRY = "https://registry.npmjs.org" SCHEMA_IN_PACKAGE = "package/schema/anvil-schema.graphql" -OUTPUT_PATH = Path(__file__).resolve().parent.parent / "schema" / "anvil_schema.graphql" +OPENAPI_URL = "https://app.useanvil.com/api/v1/openapi.json" + +SCHEMA_DIR = Path(__file__).resolve().parent.parent / "schema" +SCHEMA_PATH = SCHEMA_DIR / "anvil_schema.graphql" +OPENAPI_PATH = SCHEMA_DIR / "openapi.json" -def fetch_schema(version: str = "latest") -> tuple[str, str]: +def fetch_graphql_schema(version: str = "latest") -> tuple[str, str]: """The schema SDL in the given version of @anvilco/types, and that version.""" metadata_url = f"{REGISTRY}/{PACKAGE.replace('/', '%2F')}" - with urllib.request.urlopen(metadata_url) as response: + with urllib.request.urlopen(metadata_url, timeout=60) as response: metadata = json.load(response) if version == "latest": version = metadata["dist-tags"]["latest"] tarball_url = metadata["versions"][version]["dist"]["tarball"] - with urllib.request.urlopen(tarball_url) as response: + with urllib.request.urlopen(tarball_url, timeout=60) as response: tarball = response.read() with tarfile.open(fileobj=io.BytesIO(tarball), mode="r:gz") as archive: - member = archive.extractfile(SCHEMA_IN_PACKAGE) + try: + member = archive.extractfile(SCHEMA_IN_PACKAGE) + except KeyError: + member = None if member is None: raise FileNotFoundError( f"{SCHEMA_IN_PACKAGE} is not in {PACKAGE}@{version}" @@ -50,13 +62,34 @@ def fetch_schema(version: str = "latest") -> tuple[str, str]: return member.read().decode("utf-8"), version +def fetch_openapi_spec() -> str: + """The OpenAPI spec, pretty-printed with sorted keys so the diff is only real changes.""" + with urllib.request.urlopen(OPENAPI_URL, timeout=60) as response: + spec = json.load(response) + if "openapi" not in spec or "paths" not in spec: + raise ValueError(f"{OPENAPI_URL} did not return an OpenAPI spec") + return json.dumps(spec, indent=2, sort_keys=True) + "\n" + + def main() -> None: version = sys.argv[1] if len(sys.argv) > 1 else "latest" - schema, version = fetch_schema(version) - OUTPUT_PATH.write_text(schema) + + schema, version = fetch_graphql_schema(version) + openapi = fetch_openapi_spec() + + SCHEMA_PATH.write_text(schema, encoding="utf-8") print( - f"Wrote the schema from {PACKAGE}@{version} to {OUTPUT_PATH.relative_to(Path.cwd())}" + f"Wrote the GraphQL schema from {PACKAGE}@{version} to {_relative(SCHEMA_PATH)}" ) + OPENAPI_PATH.write_text(openapi, encoding="utf-8") + print(f"Wrote the OpenAPI spec from {OPENAPI_URL} to {_relative(OPENAPI_PATH)}") + + +def _relative(path: Path) -> str: + try: + return str(path.relative_to(Path.cwd())) + except ValueError: + return str(path) if __name__ == "__main__": diff --git a/dev/generate_types.py b/dev/generate_types.py new file mode 100644 index 00000000..06334e7f --- /dev/null +++ b/dev/generate_types.py @@ -0,0 +1,325 @@ +#!/usr/bin/env python3 +"""Generates python_anvil/types from the API specs in schema/. + +- types/rest.py: pydantic models for the REST API's request bodies, from + schema/openapi.json, with datamodel-code-generator. +- types/graphql.py: pydantic models for every type in the GraphQL schema, + objects, inputs and enums, from schema/anvil_schema.graphql, with + datamodel-code-generator. +- types/operations.py: a pydantic model of the arguments of every query and + mutation, e.g. CreateEtchPacketArgs, from the same schema with graphql-core. + +Reads the checked-in specs only, so this runs offline. `dev/fetch_schema.py` +(`make update-schema`) is what refreshes them. + +Usage: poetry run python dev/generate_types.py +""" + +from __future__ import annotations + +import json +import keyword +import re +import subprocess +import sys +from pathlib import Path + +from graphql import ( + GraphQLArgument, + GraphQLEnumType, + GraphQLInputObjectType, + GraphQLList, + GraphQLNamedType, + GraphQLNonNull, + GraphQLScalarType, + GraphQLSchema, + GraphQLType, + build_schema, +) + +# graphql-core marks an argument without a default with its own Undefined +from graphql.pyutils import Undefined +from pydantic import BaseModel + + +ROOT = Path(__file__).resolve().parent.parent +SCHEMA_PATH = ROOT / "schema" / "anvil_schema.graphql" +OPENAPI_PATH = ROOT / "schema" / "openapi.json" +TYPES_DIR = ROOT / "python_anvil" / "types" +REST_PATH = TYPES_DIR / "rest.py" +GRAPHQL_PATH = TYPES_DIR / "graphql.py" +OPERATIONS_PATH = TYPES_DIR / "operations.py" + +BASE_CLASS = "python_anvil.types._base.GeneratedModel" + +# The same for both datamodel-code-generator runs +CODEGEN_OPTIONS = [ + "--output-model-type", + "pydantic_v2.BaseModel", + "--base-class", + BASE_CLASS, + "--target-python-version", + "3.10", + "--snake-case-field", + "--allow-population-by-field-name", + "--extra-fields", + "allow", + "--use-standard-collections", + "--use-union-operator", + "--use-annotated", + "--field-constraints", + "--use-schema-description", + "--use-double-quotes", + "--disable-timestamp", + # Enums as Literal types, so the fields take and give plain strings + "--enum-field-as-literal", + "all", + # The outputs are formatted with ruff afterwards + "--formatters", + "builtin", +] + +# The schema's custom scalars, as Python types +GRAPHQL_SCALARS = { + "String": "str", + "Int": "int", + "Float": "float", + "Boolean": "bool", + "ID": "str", + "Date": "str", + "JSON": "Any", + "Upload": "Any", +} + + +def header(source: Path, extra: str = "") -> str: + return ( + f"# Generated by dev/generate_types.py from {source.relative_to(ROOT)}. Do not edit by hand.\n" + "# Run `make generate` to regenerate.\n" + extra + ) + + +def run_codegen( + input_path: Path, input_type: str, output_path: Path, *extra: str +) -> None: + subprocess.run( + [ + sys.executable, + "-m", + "datamodel_code_generator", + "--input", + str(input_path), + "--input-file-type", + input_type, + "--output", + str(output_path), + *CODEGEN_OPTIONS, + *extra, + ], + check=True, + cwd=ROOT, + ) + + +def generate_rest() -> None: + run_codegen(OPENAPI_PATH, "openapi", REST_PATH, "--collapse-root-models") + source = REST_PATH.read_text(encoding="utf-8") + # Plain strings for emails and dates. EmailStr would need the email-validator + # package, and AwareDatetime would turn a date string into a datetime that + # the JSON encoder can't send, and reject one with no timezone. + source = _use_plain_str(source, ["EmailStr", "AwareDatetime"]) + source = re.sub( + r"^# generated by datamodel-codegen:\n# filename: .*\n", + "", + source, + flags=re.M, + ) + REST_PATH.write_text(header(OPENAPI_PATH) + source, encoding="utf-8") + + +def _use_plain_str(source: str, type_names: list[str]) -> str: + """Replace pydantic types with `str`, and drop them from the pydantic import. + + The import may be on one line or wrapped over several, so the names are + removed from the import first, by whichever form they take, and then + replaced everywhere else. + """ + for name in type_names: + # `from pydantic import A, Name, B` or a wrapped ` Name,` line + source = re.sub( + rf"^(from pydantic import [^(\n]*?)\b{name}, ", r"\1", source, flags=re.M + ) + source = re.sub( + rf"^(from pydantic import [^(\n]*?), {name}\b", r"\1", source, flags=re.M + ) + source = re.sub(rf"^\s*{name},\n", "", source, flags=re.M) + source = re.sub(rf"\b{name}\b", "str", source) + return source + + +def generate_graphql() -> None: + overrides = json.dumps({"JSON": "typing.Any", "Upload": "typing.Any"}) + run_codegen( + SCHEMA_PATH, + "graphql", + GRAPHQL_PATH, + "--graphql-no-typename", + "--type-overrides", + overrides, + ) + source = GRAPHQL_PATH.read_text(encoding="utf-8") + source = re.sub( + r"^# generated by datamodel-codegen:\n# filename: .*\n", + "", + source, + flags=re.M, + ) + GRAPHQL_PATH.write_text(header(SCHEMA_PATH) + source, encoding="utf-8") + + +def generate_operations(schema: GraphQLSchema) -> None: + """A model of the arguments of every query and mutation.""" + lines = [ + header( + SCHEMA_PATH, + '"""The arguments of every query and mutation in the Anvil GraphQL schema, as pydantic models.\n\n' + "Field names are snake_case; `model_dump(by_alias=True, exclude_none=True)` gives the\n" + "camelCase variables to send. Input object and enum types come from `python_anvil.types.graphql`.\n" + '"""\n', + ), + "from __future__ import annotations", + "", + "from typing import Annotated, Any", + "", + "from pydantic import Field", + "", + "from python_anvil.types import graphql", + "from python_anvil.types._base import GeneratedModel", + "", + "", + ] + + names: dict[str, str] = {} + all_names: list[str] = [] + for operation, root in ( + ("query", schema.query_type), + ("mutation", schema.mutation_type), + ): + if root is None: + continue + for field_name, field in root.fields.items(): + class_name = f"{field_name[0].upper()}{field_name[1:]}Args" + if class_name in names: + raise ValueError(f"{class_name} would be both a query and a mutation") + names[class_name] = field_name + all_names.append(class_name) + + lines.append(f"class {class_name}(GeneratedModel):") + description = f"Arguments of the `{field_name}` {operation}." + if field.description: + description = f"{description}\n\n{_indent(field.description.strip())}" + lines.append(f' """{description}\n """') + lines.append("") + if not field.args: + lines.append(" pass") + for arg_name, arg in field.args.items(): + lines.extend(_argument_lines(arg_name, arg)) + lines.extend(["", ""]) + + lines.append("__all__ = [") + lines.extend(f' "{name}",' for name in sorted(all_names)) + lines.append("]") + lines.append("") + OPERATIONS_PATH.write_text("\n".join(lines), encoding="utf-8") + + +def _argument_lines(arg_name: str, arg: GraphQLArgument) -> list[str]: + required = isinstance(arg.type, GraphQLNonNull) and arg.default_value is Undefined + annotation = _python_type(arg.type) + if not required: + annotation = f"{annotation} | None" + + field_name = _snake_case(arg_name) + # A Python keyword, or a name pydantic's BaseModel already uses (like `schema`) + if keyword.iskeyword(field_name) or hasattr(BaseModel, field_name): + field_name = f"{field_name}_" + field_options = [f'alias="{arg_name}"'] if field_name != arg_name else [] + descriptions = [] + if arg.description: + descriptions.append(arg.description.strip()) + if arg.default_value is not Undefined: + descriptions.append( + f"Defaults to `{_graphql_literal(arg.default_value)}` when left out." + ) + if descriptions: + field_options.append(f"description={json.dumps(' '.join(descriptions))}") + + if field_options: + annotation = f"Annotated[{annotation}, Field({', '.join(field_options)})]" + default = "" if required else " = None" + return [f" {field_name}: {annotation}{default}"] + + +def _python_type(graphql_type: GraphQLType) -> str: + if isinstance(graphql_type, GraphQLNonNull): + return _python_type(graphql_type.of_type) + if isinstance(graphql_type, GraphQLList): + inner = _python_type(graphql_type.of_type) + if not isinstance(graphql_type.of_type, GraphQLNonNull): + inner = f"{inner} | None" + return f"list[{inner}]" + if isinstance(graphql_type, GraphQLScalarType): + return GRAPHQL_SCALARS.get(graphql_type.name, "Any") + if isinstance(graphql_type, (GraphQLEnumType, GraphQLInputObjectType)): + return f"graphql.{graphql_type.name}" + if isinstance(graphql_type, GraphQLNamedType): # pragma: no cover + return "Any" + raise TypeError(f"Unexpected GraphQL type {graphql_type!r}") # pragma: no cover + + +def _graphql_literal(value: object) -> str: + if isinstance(value, bool): + return "true" if value else "false" + if value is None: + return "null" + return json.dumps(value) + + +def _snake_case(name: str) -> str: + """Convert camelCase to snake_case, keeping acronyms and their plurals whole: mergePDFs -> merge_pdfs.""" + # An acronym followed by a word: "URLFor" -> "URL_For". Two lowercase letters + # or more make a word, so a plural "s" stays with its acronym. + name = re.sub(r"([A-Z]+)([A-Z][a-z]{2,})", r"\1_\2", name) + name = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", name) + return name.lower() + + +def _indent(text: str, spaces: int = 4) -> str: + return "\n".join( + f"{' ' * spaces}{line}" if line else "" for line in text.splitlines() + ) + + +def main() -> None: + TYPES_DIR.mkdir(exist_ok=True) + generate_rest() + generate_graphql() + generate_operations(build_schema(SCHEMA_PATH.read_text(encoding="utf-8"))) + + outputs = [str(REST_PATH), str(GRAPHQL_PATH), str(OPERATIONS_PATH)] + subprocess.run( + [sys.executable, "-m", "ruff", "check", "--fix", "--quiet", *outputs], + check=True, + cwd=ROOT, + ) + subprocess.run( + [sys.executable, "-m", "ruff", "format", "--quiet", *outputs], + check=True, + cwd=ROOT, + ) + for output in outputs: + print(f"Wrote {Path(output).relative_to(ROOT)}") + + +if __name__ == "__main__": + main() diff --git a/poetry.lock b/poetry.lock index 5a47c0e1..9a77b34d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -6,12 +6,27 @@ version = "0.8.0" description = "Reusable constraint types to use with typing.Annotated" optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0"}, {file = "annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7"}, ] +[[package]] +name = "argcomplete" +version = "3.7.2" +description = "Bash tab completion for argparse" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "argcomplete-3.7.2-py3-none-any.whl", hash = "sha256:6029205678bdd9c1c728a155f5f9ecf5812393f969eef58807641a2bc2aa5b19"}, + {file = "argcomplete-3.7.2.tar.gz", hash = "sha256:aad8b69a0b9969edb62db0d1752354c0d50717b10e0cbb00e2a958381b9fc6b9"}, +] + +[package.extras] +test = ["coverage", "mypy", "pexpect", "ruff", "wheel"] + [[package]] name = "ast-serialize" version = "0.13.1" @@ -79,6 +94,60 @@ files = [ {file = "ast_serialize-0.13.1.tar.gz", hash = "sha256:401cc84e8888d4d76f2c8d296fa006ed1a5ff993b6aff90dd777c03973efd025"}, ] +[[package]] +name = "black" +version = "26.10.1" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +markers = "sys_platform != \"emscripten\"" +files = [ + {file = "black-26.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:51d5e417e700fe6ec0b0ecdc408c6f6cb5def80328f31f724993d82c6486b746"}, + {file = "black-26.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:70ccbd175b7f6be29d2b727ee7ca6b4c54053df59da653a6df80b175d20a94fa"}, + {file = "black-26.10.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5bd3518d8e97138fef295230b1e9804076d69fa4e3594071494a8c68abe6266"}, + {file = "black-26.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:0ce08b367307b0fd91c9dd1d4084e62b05b3055475f951f0f34a46b6e2393b64"}, + {file = "black-26.10.1-cp310-cp310-win_arm64.whl", hash = "sha256:19fa8f5beb5e77c54c9c7e21d00cc93ed6c8b6228ee385616906d6befe081143"}, + {file = "black-26.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9a0219b29cd70e49f920acb7081e6ce5025c719008447c521d0200dcad93206a"}, + {file = "black-26.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7bdade400bfe24d78a7762896acc2f9a8e1a17fb0fd0536bf6b7c7097cf3eec7"}, + {file = "black-26.10.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ff57f63029aa1353fa8b1b0c8971fd88a6c92dc766608d2eee33ad2deb23270e"}, + {file = "black-26.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:3414a0c52901964dceabd98c7c56beac0f964115a116ecedcce7247359b14017"}, + {file = "black-26.10.1-cp311-cp311-win_arm64.whl", hash = "sha256:1935b32f5326028019856e18cb42b4da63db23765dc84464cec723e0de478a9b"}, + {file = "black-26.10.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe85fc4019bee59bc495c0f2a8ee76c5cd02c7015508d94a967ba2376f39a52c"}, + {file = "black-26.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:182f6c32be38074b16d378498c498b32cb51928178ee611485344972c35ec9c6"}, + {file = "black-26.10.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b5347d760f0c02bb00dd249384cab71c3bf828b4f68d5b401eb116e0390f147d"}, + {file = "black-26.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:4d9a90516db1d99c25dbb20cc0998e0e01531dd903466c7744e56d66f864220a"}, + {file = "black-26.10.1-cp312-cp312-win_arm64.whl", hash = "sha256:2ffbc023a12d0c729408823b8f10514490bd0baa301d0d4e21a7240249f9507f"}, + {file = "black-26.10.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b6272cfd7e1e8e271f5b0e0207259fe2834687e5cb9b5f620b34a44db9754993"}, + {file = "black-26.10.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:978113a40223a6aaefc17364176a809a320e6b288683841427fff04c6d7b4130"}, + {file = "black-26.10.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03c0ddd93bb392e71209903a691767eb366fe1a76deb9509ccbaae9e1f14bb52"}, + {file = "black-26.10.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6dba8138cdc99061ef07b958ac082d2aa057b6961d1936f9717c350f02bab5f"}, + {file = "black-26.10.1-cp313-cp313-win_arm64.whl", hash = "sha256:d42dd2fac7c342ae67e64ee99c9532e20b2a84e92c79ed3317fa2ef54c801d93"}, + {file = "black-26.10.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:8375962579d537364cc0efa19b1474481915d3a793f9fc0774901814c5e5b5f4"}, + {file = "black-26.10.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d8b3a9074a680b3c5749633714e9ae3992a1e5a23343a97ad61cd9b119b444d2"}, + {file = "black-26.10.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:289282aa2e09d3162312a3be1788ff21b08e9ea9cc4a81e656024728b32428fb"}, + {file = "black-26.10.1-cp314-cp314-win_amd64.whl", hash = "sha256:5cd88fd7b444ca51f3fc883b6f6657ea53a258b0b2eef6d9f2dfcfa17ce0e27b"}, + {file = "black-26.10.1-cp314-cp314-win_arm64.whl", hash = "sha256:2520037aa62f8a1454d0811b8f5c88b444445b03a4bfba480d8d220893b64c34"}, + {file = "black-26.10.1-py3-none-any.whl", hash = "sha256:28842f9a8207cc1df6eb983a35a14c5a0dfcd603d214fe82d84bef552afd2e3a"}, + {file = "black-26.10.1.tar.gz", hash = "sha256:5f9f83beae62437e060dafd53d7f1fc327e3d3494f74d72ee5c2b73eb90fc4e7"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=1.0.0" +platformdirs = ">=2" +pytokens = ">=0.4.0,<0.5.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.10)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2) ; sys_platform != \"win32\"", "winloop (>=0.5.0) ; sys_platform == \"win32\""] + [[package]] name = "certifi" version = "2026.10.19" @@ -435,6 +504,51 @@ tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.1 [package.extras] toml = ["tomli ; python_full_version <= \"3.11.0a6\""] +[[package]] +name = "datamodel-code-generator" +version = "0.86.0" +description = "Datamodel Code Generator" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "datamodel_code_generator-0.86.0-py3-none-any.whl", hash = "sha256:1a3d6f0a14a33e334b4251d04e67b0b6ef05c96d4625fb99bd09d716e30ea084"}, + {file = "datamodel_code_generator-0.86.0.tar.gz", hash = "sha256:1834ba0e84307dfed625eeacfbb07d790ede01a3adf5414a1c9947c5b9139078"}, +] + +[package.dependencies] +argcomplete = ">=2.10.1,<4" +black = [ + {version = ">=19.10b0", markers = "python_version < \"3.15\" and sys_platform != \"emscripten\""}, + {version = ">=26.5", markers = "python_version >= \"3.15\" and sys_platform != \"emscripten\""}, +] +genson = ">=1.2.1,<2" +inflect = ">=4.1,<8" +isort = {version = ">=4.3.21,<9", markers = "sys_platform != \"emscripten\""} +jinja2 = ">=2.10.1,<4" +pydantic = [ + {version = ">=2,<2.10 || >2.10,<2.10.1 || >2.10.1,<2.10.2 || >2.10.2,<3", markers = "python_version < \"3.14\""}, + {version = ">=2.12,<3", markers = "python_version == \"3.14\""}, + {version = ">=2.14,<3", markers = "python_version >= \"3.15\""}, +] +pyyaml = ">=6.0.1" +tomli = {version = ">=2.2.1,<3", markers = "python_version < \"3.11\""} +typing-extensions = ">=4.16,<5" + +[package.extras] +all = ["black (>=19.10b0) ; python_version < \"3.15\" and sys_platform != \"emscripten\"", "black (>=26.5) ; python_version >= \"3.15\" and sys_platform != \"emscripten\"", "graphql-core (>=3.2.3)", "grpcio-tools (>=1.62,<2)", "httpx (>=0.24.1)", "isort (>=4.3.21,<9) ; sys_platform != \"emscripten\"", "openapi-spec-validator (>=0.2.8,<0.8)", "prance (>=0.18.2)", "pysnooper (>=0.4.1,<2)", "ruff (>=0.9.10)", "watchfiles (>=1.1)"] +black = ["black (>=19.10b0) ; python_version < \"3.15\" and sys_platform != \"emscripten\"", "black (>=26.5) ; python_version >= \"3.15\" and sys_platform != \"emscripten\""] +debug = ["pysnooper (>=0.4.1,<2)"] +graphql = ["graphql-core (>=3.2.3)"] +http = ["httpx (>=0.24.1)"] +httpx2 = ["httpx2 (>=2,<3)"] +isort = ["isort (>=4.3.21,<9) ; sys_platform != \"emscripten\""] +protobuf = ["grpcio-tools (>=1.62,<2)"] +ruff = ["ruff (>=0.9.10)"] +ryaml = ["ryaml (>=0.5.1)"] +validation = ["openapi-spec-validator (>=0.2.8,<0.8)", "prance (>=0.18.2)"] +watch = ["watchfiles (>=1.1)"] + [[package]] name = "exceptiongroup" version = "1.3.1" @@ -454,6 +568,18 @@ typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} [package.extras] test = ["pytest (>=6)"] +[[package]] +name = "genson" +version = "1.4.0" +description = "GenSON is a powerful, user-friendly JSON Schema generator." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "genson-1.4.0-py3-none-any.whl", hash = "sha256:03bc71bbe52defde70660cc4dcd1ea1097997da5a1cbb90a9dbd3acc7c9e1b65"}, + {file = "genson-1.4.0.tar.gz", hash = "sha256:bc7f1c1bae87a21ca44d81149aec95a3f4468d676de9b8b08caa064f3c50b3da"}, +] + [[package]] name = "graphql-core" version = "3.3.0" @@ -481,6 +607,31 @@ files = [ [package.extras] all = ["coverage (>=7.10.0)", "hypothesis (>=6.141.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.16.0)", "ty (>=0.0.37)"] +[[package]] +name = "inflect" +version = "7.6.0" +description = "Correctly generate plurals, singular nouns, ordinals, indefinite articles" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "inflect-7.6.0-py3-none-any.whl", hash = "sha256:9fefad5c0c174d8a38f8c33d3033817e38fca8475f7f226c62efc39d46c83e8a"}, + {file = "inflect-7.6.0.tar.gz", hash = "sha256:b46875b50534a7cd5fcfb5a7c971bb93fe68948c43ca53e9833bd1e1a69221bd"}, +] + +[package.dependencies] +"jaraco.functools" = "*" +more_itertools = ">=8.5.0" +typeguard = ">=4.0.1" + +[package.extras] +check = ["pytest-checkdocs (>=2.14)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=3.4)"] +test = ["pygments", "pytest (>=6,!=8.1.*)"] +type = ["pytest-mypy (>=1.0.1) ; platform_python_implementation != \"PyPy\""] + [[package]] name = "iniconfig" version = "2.3.1" @@ -493,6 +644,63 @@ files = [ {file = "iniconfig-2.3.1.tar.gz", hash = "sha256:67f4b9c50da0dedf52af349e7749a80a9057a5031199791b906c3bb3ae878960"}, ] +[[package]] +name = "isort" +version = "8.0.1" +description = "A Python utility / library to sort Python imports." +optional = false +python-versions = ">=3.10.0" +groups = ["dev"] +markers = "sys_platform != \"emscripten\"" +files = [ + {file = "isort-8.0.1-py3-none-any.whl", hash = "sha256:28b89bc70f751b559aeca209e6120393d43fbe2490de0559662be7a9787e3d75"}, + {file = "isort-8.0.1.tar.gz", hash = "sha256:171ac4ff559cdc060bcfff550bc8404a486fee0caab245679c2abe7cb253c78d"}, +] + +[package.extras] +colors = ["colorama"] + +[[package]] +name = "jaraco-functools" +version = "4.6.0" +description = "Functools like those found in stdlib" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "jaraco_functools-4.6.0-py3-none-any.whl", hash = "sha256:99e3dc0060c5cbe8fcd1cdb36258e2a65ca40f1566b2033b12abb1bb44dd3c30"}, + {file = "jaraco_functools-4.6.0.tar.gz", hash = "sha256:880c577ec9720b3a052d5bc611fb9f2269b3d87902ef42440df443b88e443280"}, +] + +[package.dependencies] +more_itertools = "*" + +[package.extras] +check = ["pytest-checkdocs (>=2.14)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=3.4)"] +test = ["jaraco.classes", "pytest (>=6,!=8.1.*)"] +type = ["pytest-mypy (>=1.0.1) ; platform_python_implementation != \"PyPy\""] + +[[package]] +name = "jinja2" +version = "3.1.6" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, + {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + [[package]] name = "librt" version = "0.16.0" @@ -641,6 +849,193 @@ files = [ {file = "librt-0.16.0.tar.gz", hash = "sha256:ac38d6d8d66bf3d744148dbbc0b8e193e195a51e364ed55e224631f5721891fc"}, ] +[[package]] +name = "markupsafe" +version = "3.0.4" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "markupsafe-3.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd8ea6ebee7aedbf7c749fa80521d9ccf1ba473e0d1e14805caafbaad281c889"}, + {file = "markupsafe-3.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dff05cb7016dff1e9fd68f4122c127b65dfc59de5306cfb7ad92f956f230bee2"}, + {file = "markupsafe-3.0.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cf63c214fe879a65e69a386f915e36104fc84254ab141240f8854602d8e0be2a"}, + {file = "markupsafe-3.0.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2a6ef68ae94aed8721934072b27a3b654ea2100b97e4ab864cf1489c90926fbc"}, + {file = "markupsafe-3.0.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd9f8797427910198f95bced71ddfed61130d7e349213bfb8466c9c99e2c46a8"}, + {file = "markupsafe-3.0.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d1aca03ede943eb80ab3d63bb082c84b7aab85ea83bd0fd0c200260945fb49d9"}, + {file = "markupsafe-3.0.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0764a13d34cae40db7bbf3a09b7e9b491bf4603e20b263a7a9d6b8e324975d0a"}, + {file = "markupsafe-3.0.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9388003072b95f2f1e3fd908604194d653ba21330d811961a78b7da1a77e9e36"}, + {file = "markupsafe-3.0.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:8698d70a8081ee8c090dbb394768b5789a1da8b131b5499f89d071dd3cfaf6be"}, + {file = "markupsafe-3.0.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:bf053da3c97a4bc5ecfbb218cdd2983febd91c617be8367d139882aa11e490aa"}, + {file = "markupsafe-3.0.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:9438a2648b2195980cb2dd8e53ed7b8df91319e2d0b70ae61a9e1d1bc8d3bec9"}, + {file = "markupsafe-3.0.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:88d59b473bfb03259722600839af9bbd7fa13a2eb514beefeedb95997882f69a"}, + {file = "markupsafe-3.0.4-cp310-cp310-win32.whl", hash = "sha256:4a540e2d3192792fc84eced57bef37851ccb2b41f73291bb17408eea77bcd278"}, + {file = "markupsafe-3.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:5c22873ad1f0532ba40fa1727f3c0fc1bbbaab6d373d4cbe3f0dc74b2e2521c7"}, + {file = "markupsafe-3.0.4-cp310-cp310-win_arm64.whl", hash = "sha256:3d23795802fc8bd72534836d64489bbf0f67c088959091bdb22e10735a5107bf"}, + {file = "markupsafe-3.0.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9e25feb9e330b63edb0278a0acdf85e50d0cb0fbf49c3084abbe4e24ae195346"}, + {file = "markupsafe-3.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7d3391b2188d18737cb2fa147028b1096236eaa7e156446c650a489fa2cadc91"}, + {file = "markupsafe-3.0.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:849dd2bb0e5e4ab2b71c7191726a4a8d5aa8a610daa584728cbee0b710ddc4ef"}, + {file = "markupsafe-3.0.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:befb4158af32106b9a93db8d6d1d1cbbd418c0d5aca0cabb7b1780abf0c89169"}, + {file = "markupsafe-3.0.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:71f88e749ea29f67f21f3b36433c1dc54c7729ed2a6d9e2da2e0d9e0d7b224eb"}, + {file = "markupsafe-3.0.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6da83a088f8ef93b2d483a8232a4dbf4d69d3d8496b568a03c56becac43e1808"}, + {file = "markupsafe-3.0.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8f0fac8b13d14bb06c68195f849371924ae53dd7b1c00fed24650f704383b692"}, + {file = "markupsafe-3.0.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4a7cdc2a420ca01058182da4253329764d4bfa055564d1eced90e6ba1e8b1d3d"}, + {file = "markupsafe-3.0.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:83b3944fea42a8400edf92fd1770fb8d0d4f7de651353bd2d8525a92dba69a21"}, + {file = "markupsafe-3.0.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:8138eb83940ec7299024d92d4dee45f601b9e6c5ffde9d25f4e35e326203c707"}, + {file = "markupsafe-3.0.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:811d02d5122171c1941357efd8f9bf4ffe907b7f0a1a4e729a880e4be3f46e3e"}, + {file = "markupsafe-3.0.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:50b5bedc9ed8a94fc8857a42ef4f84a81ea88f8d4f05dc8705fb23ee6d8dcca7"}, + {file = "markupsafe-3.0.4-cp311-cp311-win32.whl", hash = "sha256:2e5a7cd7fdd14fcb1ae5d7d8bf23d24fbd1daefd1fbca2580132e1ea75f098b5"}, + {file = "markupsafe-3.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:fdb4ca07ab75ffadab4a8b135ad59cdbb3156b99310f3d565370da74a15d6bd3"}, + {file = "markupsafe-3.0.4-cp311-cp311-win_arm64.whl", hash = "sha256:569d65055d367e3dcdf30c3f41119467b73d9ee9faf332bdf40402644f5ac08e"}, + {file = "markupsafe-3.0.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:61631e08084be9e21a8967ec3139c7616ed7c5e9368e05c86d1b39562c8a57b6"}, + {file = "markupsafe-3.0.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0930db9bdc62d22944e10b066448bb65dc9abe9112880c7cab8da54db4284d5f"}, + {file = "markupsafe-3.0.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a45c3d514f2436064db00d7fc8778d888f0236ebfed649b53d13a59e69ad51b"}, + {file = "markupsafe-3.0.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1e1451fab512d1bcc3dc26988ec1edb0b82c2db909132872cd9356070a6b63df"}, + {file = "markupsafe-3.0.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bd3ce56ae2cbae3ba82b683bc425cd7e48d2ed8b10f3e818186b6f5646d9271c"}, + {file = "markupsafe-3.0.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e124f974786f831d6043728e38296969d3579db8896fe004682f5758e613581"}, + {file = "markupsafe-3.0.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c02e8f18bdedba082cef725942ac823b9b60656db07f7e265cb31618dfd00d77"}, + {file = "markupsafe-3.0.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f098115c247e11d138ab83a28fa0323c77015007ea2df73ba5fd714dfefd67c"}, + {file = "markupsafe-3.0.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d5f93ebbeb8032d47e349328ec8662d973d9b05a70b3c35df1f91fe419b84749"}, + {file = "markupsafe-3.0.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:64511c54db4e4987aef4c41923235927428729e8174c5dba488429be70a998ed"}, + {file = "markupsafe-3.0.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e1a622f13970d81f95d0c72f9dc090dce9085fccfa4c9f2174377ee32bd15786"}, + {file = "markupsafe-3.0.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c9a7f43c0b202b334cc9184af09bb8f21d3a209e038efaf106936fb69e6b026e"}, + {file = "markupsafe-3.0.4-cp312-cp312-win32.whl", hash = "sha256:f0ec3b750b59375eab5b0fb2b9254810c00a3375be6d789899f1055a1d556237"}, + {file = "markupsafe-3.0.4-cp312-cp312-win_amd64.whl", hash = "sha256:11935df9bf455ed0c04eb87bcd720f02b1fe5e02128a9430f23aed6f93336fc7"}, + {file = "markupsafe-3.0.4-cp312-cp312-win_arm64.whl", hash = "sha256:a4bbd2d87dd233b9fc5812160c3d0ffbe42edc22a26ce0469f58479ede633fe9"}, + {file = "markupsafe-3.0.4-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:de8b364c423ef0a4bad9069657d617f9a5d2b2062457a89b1fa16ee199c399c1"}, + {file = "markupsafe-3.0.4-cp313-cp313-android_24_x86_64.whl", hash = "sha256:34bdde374c5932765d7dc685c4a1d191a3207852d67e8e0a9eb6ea85156181f1"}, + {file = "markupsafe-3.0.4-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:6bd9e1788e15bfcf6a9082de42e30387e7b85d211ab21e57a939bb8cfaaf8d96"}, + {file = "markupsafe-3.0.4-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:5066b244f576f91afc8ee3ba029a89f99d39c79b1853fe9d39bea9f0afbec148"}, + {file = "markupsafe-3.0.4-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7a83aa6e4805df46fed18e989d3d16f86ef60cb50bbc8d9ce3a6be89165fbf6e"}, + {file = "markupsafe-3.0.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2d1b7d9308288661f56672b1b157d75fc536714d3638487bbea17b6318a78248"}, + {file = "markupsafe-3.0.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73e77980c7207854f00fc4e71fb1626868d5740ab4012623d55c7a99ad122a72"}, + {file = "markupsafe-3.0.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7018d4af1cd272e847aa5917983ab5e83e4f6579f9dbfecd4a79c0ca80b144c2"}, + {file = "markupsafe-3.0.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c90d5b3d4e944e065a301d741b3c1d784f6bd1f503aa68b4967e32b2ba313d85"}, + {file = "markupsafe-3.0.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:18a801868a884f216e784d7d14db2a4077143ce7610440aee2ce8f734e7cfcde"}, + {file = "markupsafe-3.0.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:434139499bb20b502ed3baa1f169e618f924a97e7a777fea1a49446d80106cf6"}, + {file = "markupsafe-3.0.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9e227f3dbe6bde7491cf0a9965d00b88c6b1a4a95d11480ddf88bb96d397c19f"}, + {file = "markupsafe-3.0.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b8cd1f918b26fd7b1832ece557cc18f2d8747309ff8b3f0ef9d4250c5ad67a39"}, + {file = "markupsafe-3.0.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a5fcffb37e602b0b3c1638a97746b9b96125caa9bcf6fa41d337a9261de231ee"}, + {file = "markupsafe-3.0.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5989cb26b2e1efc6a42216a9f6b5ee495ce5ace2e5b352a9af489976b32d1ee2"}, + {file = "markupsafe-3.0.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:add96447a86d205ab616665d53b2950ee81083757f56e6ea833c8b2917646b46"}, + {file = "markupsafe-3.0.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2628d3a8cb648ecebb3c5d6b0a1052d400e4d8b7ac0fb786be8d285b50040d17"}, + {file = "markupsafe-3.0.4-cp313-cp313-win32.whl", hash = "sha256:672d207103e6b16ca098611b0f9efad6bc00afd47c03d6ef62186495ca677dc0"}, + {file = "markupsafe-3.0.4-cp313-cp313-win_amd64.whl", hash = "sha256:1f1f9477e174582b0a1b583d60b66e1f2cf5d3fe12cee985e4aedf44766600e5"}, + {file = "markupsafe-3.0.4-cp313-cp313-win_arm64.whl", hash = "sha256:06de8ef6331f6e822c28d577dc8bf43fe398800477c49498f38fc38b67ff33fc"}, + {file = "markupsafe-3.0.4-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:4ed644d75aa94a2baf7ec3a96eaa160ea58c742eb9d27c6506053c5c40fc84ed"}, + {file = "markupsafe-3.0.4-cp314-cp314-android_24_x86_64.whl", hash = "sha256:6d2a9efe686f9de00d0d1ea32a4a5a86d558a2277501bd78d964214eab625e59"}, + {file = "markupsafe-3.0.4-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8781a792a070cf2bd1b86d3aa943894115faaba6e88122a7bf32d62072742453"}, + {file = "markupsafe-3.0.4-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:971a3bbb75d97ae4e2e8f7d4834236f86f85f0c85e04ab2e191db1123b04f80b"}, + {file = "markupsafe-3.0.4-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:8909c2f1c6dd65e054ac4b573a91c8384d1492281e55d82d159d653f7a13adf6"}, + {file = "markupsafe-3.0.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4cf3468d5ec187ffffcaca8e61929a37448f215dafc1386a12c750a72fe53634"}, + {file = "markupsafe-3.0.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:52704c5d36eb6dda8866493decd61111fff86244c9b1ad225ca01b9e91e5970f"}, + {file = "markupsafe-3.0.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1caa2fa5a6184fb233153b35f654e6687bd555476f6170f29d8ee9be1a8b0af9"}, + {file = "markupsafe-3.0.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:387d8cd30e69b3f0a72877b9ae717033396404e19095b17fe89753a981fda44f"}, + {file = "markupsafe-3.0.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:051417f74bcaaefa316276e0ff723f541616ca51043d070da00249d9bddd3e3c"}, + {file = "markupsafe-3.0.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8e9f292fcda89b324f2f5c91d13f1424a153e40fc2756f38ee23b15835ff300"}, + {file = "markupsafe-3.0.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:df1ae86ff54725a01fa1a0510b914ca53a161b7050be74f6204e24aded5971d0"}, + {file = "markupsafe-3.0.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8965520ac587c94a4ac48b729be3d8b8de00af39699b17585dfb599babe77977"}, + {file = "markupsafe-3.0.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:340cbb1957ba99929cbf19a75626d36ba1ae21d1730b287d1cf7f824a20c4fc7"}, + {file = "markupsafe-3.0.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:3a93d9616ddecfb393727a0041a562cf0b15a244e20f2bd25efc7949be4c4f17"}, + {file = "markupsafe-3.0.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d2e56fd3b00222722abfb3f5f0759ddbae4b90811b5ad4343c64030ad1bde70c"}, + {file = "markupsafe-3.0.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0d9c47709875fdb321452056622e930c52afbc07a7d780762fbb8b4d91ce6fa4"}, + {file = "markupsafe-3.0.4-cp314-cp314-win32.whl", hash = "sha256:38fc55594dab834470b6733dead2ee9e3f657fb0608c769dcafa0ba5ab52f45c"}, + {file = "markupsafe-3.0.4-cp314-cp314-win_amd64.whl", hash = "sha256:c1bc67752d5f21013cfe430df4062441714eab79f65a6a05e01505957e9c35fe"}, + {file = "markupsafe-3.0.4-cp314-cp314-win_arm64.whl", hash = "sha256:7e1636da3d8dfc220b6dd10264db5f2b165e4888c4518594898fbe381049af8a"}, + {file = "markupsafe-3.0.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:805c8b84534fa10891890f0e4be39f3a99e94615d93e8836bf9fa1fdca2feeb2"}, + {file = "markupsafe-3.0.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fa95848c929b6a75f6848d3c9793e59db365ee436776e57db835cdbfa79ba977"}, + {file = "markupsafe-3.0.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e916035e3e9930cbdfdd10abf48861340221857f45509565898e012263f7b289"}, + {file = "markupsafe-3.0.4-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b4d12837e0203bbace818ff4a7461afdcd78bcd782351cea148139180d7bcffe"}, + {file = "markupsafe-3.0.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5086f9975abb1ab531ee6afca1761e4b59a19b446f3f6522ed776963228cfe5a"}, + {file = "markupsafe-3.0.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b4a635a0487774f841cb1fb62e907e7195cc95bc761e053184b8acc3ceb20733"}, + {file = "markupsafe-3.0.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb96e6e088d6cf71c1ea977510948320234824cf226e32f6f6e044f7a9c82b34"}, + {file = "markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8b5d563170ff8ba3181caa967c99a3c804d1dedb702c7cb93a6a7c32247da978"}, + {file = "markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:396ec4e65cc889f69786b3b89478b471cee5a3bcf468b9d9bb03e1a30fb291fc"}, + {file = "markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:15ba9e28640feef770374b116a6f019c21f52404aeabe516aa7f800587b98cfc"}, + {file = "markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d920abdfa61279ba1a2ef9484aab07bf03331f8c08a10120fa332353d06e6932"}, + {file = "markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a9f54054101545a9a9cccefddf54316aa6e4491611fcbef9e91b3b6bebec04f6"}, + {file = "markupsafe-3.0.4-cp314-cp314t-win32.whl", hash = "sha256:12a606a492de952afcb43b59a14aaaaad120e708d3663dd0fdf2d738d427a691"}, + {file = "markupsafe-3.0.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a18f38cafc329bac5e3c2b96c765b4c96d3d103421ed22ab7988c1e3fce27464"}, + {file = "markupsafe-3.0.4-cp314-cp314t-win_arm64.whl", hash = "sha256:eba154571c16e032112afac0dc2dfe9e63c2ceb7aedd07bb7eecf2ce26d4dd4c"}, + {file = "markupsafe-3.0.4-cp315-cp315-android_24_arm64_v8a.whl", hash = "sha256:737c9c3981998eba27f11786f84fddcbabc74068b72a4a1f454ea02094b57b65"}, + {file = "markupsafe-3.0.4-cp315-cp315-android_24_x86_64.whl", hash = "sha256:489505b03f692c3f376394e49194fa7a7f9e8558d6e293a7056a0032b0c38163"}, + {file = "markupsafe-3.0.4-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:077293e425f28ec737dbcad442a71752e28f8ae27cde3d68acd1fb212091cd92"}, + {file = "markupsafe-3.0.4-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9348cbb300d224fe3b89793262cb093504d4ae927004468463f745188a193e4a"}, + {file = "markupsafe-3.0.4-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:b807e598953730f82e4eae3bd30f6a122cf6b31c398c6b504c0e04c13c170429"}, + {file = "markupsafe-3.0.4-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:799c39bdf5e2f1292fedd3009f7b3c9e760f10b2420cb9638d56920840ff6db8"}, + {file = "markupsafe-3.0.4-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:ae9dcb8fbe244cb82f8a6458b455b927a03685e383d9bacf1ea5ce180b96dc97"}, + {file = "markupsafe-3.0.4-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bced6e2a6dba6a28f7dd3c6ce14df1b2dd495923f16ea484cad03decd463b2b"}, + {file = "markupsafe-3.0.4-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3882fb412298575bae3b9c46868251f15cc69307359f87bb1b382e53d6e5a2c9"}, + {file = "markupsafe-3.0.4-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04e7902ba80ee4bac1d50a549606527a1dcf0476cd81403db41099d3b60ec653"}, + {file = "markupsafe-3.0.4-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:925f929d6b59a8b3f8b8c6ac363cd0af7eecc81efb3071770b3c6717c450a369"}, + {file = "markupsafe-3.0.4-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f68edfc67aabac33708941f26f22a7b8e9f81429bc0cf249fcf7d66b23af8d19"}, + {file = "markupsafe-3.0.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:e5c802729725bd07e2bc3ab7b76dc7e0bbfc53129d8f1eb1c002c24cf774717e"}, + {file = "markupsafe-3.0.4-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:55ffd6ce583d97dc71dc92e930324c8c0d25aea7e3ade6ae54ef77cedb096811"}, + {file = "markupsafe-3.0.4-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:2cb3dd71fc6be918ad4264346a8ed69485f9b7ed7bf35495d8e22807cd6b8bea"}, + {file = "markupsafe-3.0.4-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:94f5407f7bc64fa6463906b896f9904beeeb7dd8dc116ee8e9056c8714ff9916"}, + {file = "markupsafe-3.0.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2dad610540cb2e6272855c178f08ae9a1c7ac258a7fb71660553a5f104b42741"}, + {file = "markupsafe-3.0.4-cp315-cp315-win32.whl", hash = "sha256:03470d1a8268e692ecf79ecd565593e59d44219377a7ead61f1f1b94c1f7ff6b"}, + {file = "markupsafe-3.0.4-cp315-cp315-win_amd64.whl", hash = "sha256:d882a373d8093c2941e01291b7ced96e9cbe4781da9a7751ca7e6c70385e5214"}, + {file = "markupsafe-3.0.4-cp315-cp315-win_arm64.whl", hash = "sha256:353bd63081912ab8cfa6a0c7d185934cdf8426f04c618bba6bc4b394f2069b67"}, + {file = "markupsafe-3.0.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c61750fadcd119d0825bcb7d7d675dd264dcc89cc05292aab5be68ebdbb374ad"}, + {file = "markupsafe-3.0.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:1c0df495a977d10460a94941799c72d5b5ab03d3858d949b55b5a66c8f371c99"}, + {file = "markupsafe-3.0.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02fa4acbc6a3fc5c693c34d4dd8c1130b7fe99cc915181b0ddd6f72aeb296002"}, + {file = "markupsafe-3.0.4-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:05295589e619b9bed252a86b532b8e27350abc372d18ba89b59375325e91ec1e"}, + {file = "markupsafe-3.0.4-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:be6cb0c799abb0e2ba3e618e6d28ddddf7e485f6c2ce938dfa237daf3905072c"}, + {file = "markupsafe-3.0.4-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:26e9867520db70d37f7fb421a7f0d8adb40171011fb84ce869afa1a83370dfa8"}, + {file = "markupsafe-3.0.4-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f03460ff076f70ab595bb45a0205ccea1971443575b6920c52e755dec2b3fbfe"}, + {file = "markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:436e3ffc6310d3c41878c601db29098102fe5d8a467c49da4a4125254e0980f2"}, + {file = "markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:4e2c4809c14559aa7ef426f27fb35afbb38104c349a903bf8f3600456764bb38"}, + {file = "markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:da2af0d7aebfc2074080d72efa6ab8317c62481ef1f896f65d9999c1c01f4494"}, + {file = "markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:aa2c838cc024642cc04c6854232f32b43e5e22833dd11119c1766c7873b8370d"}, + {file = "markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:b91cc9d336957239ff200f30097e6fea2dc6d6fb3c81e853eaa09eac904fd894"}, + {file = "markupsafe-3.0.4-cp315-cp315t-win32.whl", hash = "sha256:e49fb0d1ce92cfa0cb198cc5b1b11cdf9d0638658e2a2db2687e39db7c87fc78"}, + {file = "markupsafe-3.0.4-cp315-cp315t-win_amd64.whl", hash = "sha256:4f6e0852a0283b1b1fd776eeb7b766a5f440b3e2bd31ab51af3b400585f3965c"}, + {file = "markupsafe-3.0.4-cp315-cp315t-win_arm64.whl", hash = "sha256:39dbacefc411633db5b4378b066a9aca70a3d7e2922c9e578d825f844026eeba"}, + {file = "markupsafe-3.0.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f291bcf42ae98eb5107edb162c3c998b4a89648fd8e99ed4cbd12705292788cd"}, + {file = "markupsafe-3.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ac0c7c9f1609b0c4c114feb1d7a3409564c7fb77e360bed9e97e5d25dfeaf868"}, + {file = "markupsafe-3.0.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6768d67d1bce64270e0fdc2e69309d68b9b18ae56ddf6c711d168e9d051c2cac"}, + {file = "markupsafe-3.0.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:14bd2d845d62ab678eaf81da89d7b621b51756c72346745c1a594c09d49207a2"}, + {file = "markupsafe-3.0.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:007e1ffd9bf65bb6ee96df7b258fc632a4868dd5566037986c64781f35a36e98"}, + {file = "markupsafe-3.0.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e8b3d0b18fd623afa12ecb2ce8d8becef69f9b5440c6330c7972200e0bb84b0"}, + {file = "markupsafe-3.0.4-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:57f9947a7e57a081c1e3e0a2dd0d2dcf290a4531450e6f611e30084c222a7295"}, + {file = "markupsafe-3.0.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b61687d0828e72bf5cda24a2690188f37170bd31c9359ac97e4e66569f120a16"}, + {file = "markupsafe-3.0.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:0cee7cb0f9a1b6892ea482237d9403b3d1b4603aee057d0ff01f0fac2d019a97"}, + {file = "markupsafe-3.0.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:94e4c421742086aeee4c32a506eec8859d7634aad943f7e6aacf70f813478768"}, + {file = "markupsafe-3.0.4-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:9240187afb63d2f9ddc3e032c670356fe941f6e20662ea168a5dc3f1f317e1b3"}, + {file = "markupsafe-3.0.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e841068dc0be4cb6dfb5c890eb88cbdcff2f4a332393c7ec94e8e618bd32c1a8"}, + {file = "markupsafe-3.0.4-cp39-cp39-win32.whl", hash = "sha256:f61efe1d2fe0de16158a5fe1d1cf3c14bdb6aecd54d8938fd26512c525c1f624"}, + {file = "markupsafe-3.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:2b2b1e18af909b448bb3cf9e3433366f7a8726271fc214e8b10e0f62a78c724b"}, + {file = "markupsafe-3.0.4-cp39-cp39-win_arm64.whl", hash = "sha256:6669c1bf34080161ce49c589cc512ef24d4c704ac9d2b2d3667f519c60418378"}, + {file = "markupsafe-3.0.4.tar.gz", hash = "sha256:2e9ad7dd851bf45fab9f75cbff4cb493fee9979e8d8c7c9c3ee119022518edd6"}, +] + +[[package]] +name = "more-itertools" +version = "11.1.0" +description = "More routines for operating on iterables, beyond itertools" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +markers = "python_version < \"3.14\"" +files = [ + {file = "more_itertools-11.1.0-py3-none-any.whl", hash = "sha256:4b65538ae22f6fed0ce4874efd317463a7489796a0939fa66824dd542125a192"}, + {file = "more_itertools-11.1.0.tar.gz", hash = "sha256:48e8f4d9e7e5878571ecf6f2b4e57634f93cd474cc8cfbd2376f2d11b396e30d"}, +] + +[[package]] +name = "more-itertools" +version = "11.2.1" +description = "More routines for operating on iterables, beyond itertools" +optional = false +python-versions = ">=3.11" +groups = ["dev"] +markers = "python_version >= \"3.14\"" +files = [ + {file = "more_itertools-11.2.1-py3-none-any.whl", hash = "sha256:35a7377edd1dd6608dcb2cdf534ded55ea32d49448875ad042cd3f879fb1ded0"}, + {file = "more_itertools-11.2.1.tar.gz", hash = "sha256:cbf08fd0af284dc69718b9b76a8e6203df624d70209ea511e4219d350c856f63"}, +] + [[package]] name = "mypy" version = "2.4.0" @@ -762,6 +1157,32 @@ hyperscan = ["hyperscan (>=0.7)"] optional = ["typing-extensions (>=4)"] re2 = ["google-re2 (>=1.1)"] +[[package]] +name = "platformdirs" +version = "4.12.4" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +markers = "python_version < \"3.14\" and sys_platform != \"emscripten\"" +files = [ + {file = "platformdirs-4.12.4-py3-none-any.whl", hash = "sha256:78bfb9db2a8471ed7eebe3c3c932da413911042994e699b384fbb4493fa872d7"}, + {file = "platformdirs-4.12.4.tar.gz", hash = "sha256:63743c02414e755de4e31b8f68125c1407495b86c5a006e203c01ff8b9924250"}, +] + +[[package]] +name = "platformdirs" +version = "4.13.5" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.11" +groups = ["dev"] +markers = "python_version >= \"3.14\" and sys_platform != \"emscripten\"" +files = [ + {file = "platformdirs-4.13.5-py3-none-any.whl", hash = "sha256:53d3ec4280b815c1e0b02edad7f9ee8fda1ba9c0865dd9c2e6e63849ce82fc1e"}, + {file = "platformdirs-4.13.5.tar.gz", hash = "sha256:05831adf87d392760f4b5c3a0a95579ab5a10b18b9b9bd43680dcb47544788a0"}, +] + [[package]] name = "pluggy" version = "1.7.0" @@ -780,7 +1201,7 @@ version = "2.14.1" description = "Data validation using Python type hints" optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "pydantic-2.14.1-py3-none-any.whl", hash = "sha256:9195d967ec791692a04438115466764fb8b9a27b31f14a760437694f40d6b454"}, {file = "pydantic-2.14.1.tar.gz", hash = "sha256:94f478203dd03404682a1ada216965651dd74b1d2d5ffd62e00e0837caab5c26"}, @@ -802,7 +1223,7 @@ version = "2.50.1" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "pydantic_core-2.50.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b281a3b0f0822618fe5e3e0d8a2048b6356b14388505dc9374ccffeb69989713"}, {file = "pydantic_core-2.50.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fa4c8bc12c1354c5550c0c35c1852c8c1901e89e06561724e03f8d0342e1f87"}, @@ -1035,6 +1456,145 @@ files = [ [package.dependencies] pytest = ">=7.0.0" +[[package]] +name = "pytokens" +version = "0.4.1" +description = "A Fast, spec compliant Python 3.14+ tokenizer that runs on older Pythons." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +markers = "sys_platform != \"emscripten\"" +files = [ + {file = "pytokens-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a44ed93ea23415c54f3face3b65ef2b844d96aeb3455b8a69b3df6beab6acc5"}, + {file = "pytokens-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:add8bf86b71a5d9fb5b89f023a80b791e04fba57960aa790cc6125f7f1d39dfe"}, + {file = "pytokens-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:670d286910b531c7b7e3c0b453fd8156f250adb140146d234a82219459b9640c"}, + {file = "pytokens-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4e691d7f5186bd2842c14813f79f8884bb03f5995f0575272009982c5ac6c0f7"}, + {file = "pytokens-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:27b83ad28825978742beef057bfe406ad6ed524b2d28c252c5de7b4a6dd48fa2"}, + {file = "pytokens-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d70e77c55ae8380c91c0c18dea05951482e263982911fc7410b1ffd1dadd3440"}, + {file = "pytokens-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a58d057208cb9075c144950d789511220b07636dd2e4708d5645d24de666bdc"}, + {file = "pytokens-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b49750419d300e2b5a3813cf229d4e5a4c728dae470bcc89867a9ad6f25a722d"}, + {file = "pytokens-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9907d61f15bf7261d7e775bd5d7ee4d2930e04424bab1972591918497623a16"}, + {file = "pytokens-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:ee44d0f85b803321710f9239f335aafe16553b39106384cef8e6de40cb4ef2f6"}, + {file = "pytokens-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083"}, + {file = "pytokens-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1"}, + {file = "pytokens-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1"}, + {file = "pytokens-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ad948d085ed6c16413eb5fec6b3e02fa00dc29a2534f088d3302c47eb59adf9"}, + {file = "pytokens-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:3f901fe783e06e48e8cbdc82d631fca8f118333798193e026a50ce1b3757ea68"}, + {file = "pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b"}, + {file = "pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f"}, + {file = "pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1"}, + {file = "pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4"}, + {file = "pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78"}, + {file = "pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321"}, + {file = "pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa"}, + {file = "pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d"}, + {file = "pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324"}, + {file = "pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9"}, + {file = "pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb"}, + {file = "pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3"}, + {file = "pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975"}, + {file = "pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a"}, + {file = "pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918"}, + {file = "pytokens-0.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:da5baeaf7116dced9c6bb76dc31ba04a2dc3695f3d9f74741d7910122b456edc"}, + {file = "pytokens-0.4.1-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11edda0942da80ff58c4408407616a310adecae1ddd22eef8c692fe266fa5009"}, + {file = "pytokens-0.4.1-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0fc71786e629cef478cbf29d7ea1923299181d0699dbe7c3c0f4a583811d9fc1"}, + {file = "pytokens-0.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dcafc12c30dbaf1e2af0490978352e0c4041a7cde31f4f81435c2a5e8b9cabb6"}, + {file = "pytokens-0.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:42f144f3aafa5d92bad964d471a581651e28b24434d184871bd02e3a0d956037"}, + {file = "pytokens-0.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:34bcc734bd2f2d5fe3b34e7b3c0116bfb2397f2d9666139988e7a3eb5f7400e3"}, + {file = "pytokens-0.4.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:941d4343bf27b605e9213b26bfa1c4bf197c9c599a9627eb7305b0defcfe40c1"}, + {file = "pytokens-0.4.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3ad72b851e781478366288743198101e5eb34a414f1d5627cdd585ca3b25f1db"}, + {file = "pytokens-0.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:682fa37ff4d8e95f7df6fe6fe6a431e8ed8e788023c6bcc0f0880a12eab80ad1"}, + {file = "pytokens-0.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:30f51edd9bb7f85c748979384165601d028b84f7bd13fe14d3e065304093916a"}, + {file = "pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de"}, + {file = "pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a"}, +] + +[package.extras] +dev = ["black", "build", "mypy", "pytest", "pytest-cov", "setuptools", "tox", "twine", "wheel"] + +[[package]] +name = "pyyaml" +version = "6.0.3" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f"}, + {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4"}, + {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3"}, + {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6"}, + {file = "PyYAML-6.0.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369"}, + {file = "PyYAML-6.0.3-cp38-cp38-win32.whl", hash = "sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295"}, + {file = "PyYAML-6.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b"}, + {file = "pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b"}, + {file = "pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956"}, + {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8"}, + {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198"}, + {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b"}, + {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0"}, + {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69"}, + {file = "pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e"}, + {file = "pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c"}, + {file = "pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e"}, + {file = "pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824"}, + {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c"}, + {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00"}, + {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d"}, + {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a"}, + {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4"}, + {file = "pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b"}, + {file = "pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf"}, + {file = "pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196"}, + {file = "pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0"}, + {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28"}, + {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c"}, + {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc"}, + {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e"}, + {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea"}, + {file = "pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5"}, + {file = "pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b"}, + {file = "pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd"}, + {file = "pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8"}, + {file = "pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1"}, + {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c"}, + {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5"}, + {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6"}, + {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6"}, + {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be"}, + {file = "pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26"}, + {file = "pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c"}, + {file = "pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb"}, + {file = "pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac"}, + {file = "pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310"}, + {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7"}, + {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788"}, + {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5"}, + {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764"}, + {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35"}, + {file = "pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac"}, + {file = "pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3"}, + {file = "pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3"}, + {file = "pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba"}, + {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c"}, + {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702"}, + {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c"}, + {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065"}, + {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65"}, + {file = "pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9"}, + {file = "pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b"}, + {file = "pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da"}, + {file = "pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917"}, + {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9"}, + {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5"}, + {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a"}, + {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926"}, + {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7"}, + {file = "pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0"}, + {file = "pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007"}, + {file = "pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"}, +] + [[package]] name = "requests" version = "2.34.2" @@ -1176,6 +1736,21 @@ files = [ {file = "tomli-2.5.0.tar.gz", hash = "sha256:264507556cd8b8c8e7c6ee037cdf443a463f03f4c958e57195e3d369711b8ff6"}, ] +[[package]] +name = "typeguard" +version = "4.6.0" +description = "Run-time type checker for Python" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "typeguard-4.6.0-py3-none-any.whl", hash = "sha256:79878165bb86f2cf5d41d159a0ff1792a796cf496882d2fe1b1c6c7049b9cdd7"}, + {file = "typeguard-4.6.0.tar.gz", hash = "sha256:e7414f09111317de3e335de92cd397c5c0ca00b1cc1676de12e1d444a79b3f21"}, +] + +[package.dependencies] +typing_extensions = ">=4.14.0" + [[package]] name = "types-requests" version = "2.33.0.20261006" @@ -1221,7 +1796,7 @@ version = "0.4.4" description = "Runtime typing introspection tools" optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147"}, {file = "typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47"}, @@ -1254,4 +1829,4 @@ cli = ["click", "tabulate"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<3.16" -content-hash = "fe738c8cceafaf4bfa8df11ca65a70b619ed65ec1dfd01bed7e7a65bc36d87d0" +content-hash = "763a763217229c900896b5bc45df05a804d656d5400bd7ceb32bd4c576420b0b" diff --git a/pyproject.toml b/pyproject.toml index 9d4a10f1..fa9e899e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,8 @@ requires-python = ">=3.10,<3.16" dependencies = [ "requests>=2.28.2,<3", "pydantic>=2.8,<3", + # The generated types use TypeAliasType on Python 3.10 and 3.11 + "typing-extensions>=4.12", ] [project.optional-dependencies] @@ -56,8 +58,11 @@ pytest-describe = ">=2.2" pytest-timeout = ">=2.3" types-requests = ">=2.28" types-tabulate = ">=0.9" -# To check the client's queries and mutations against the API schema +# To check the client's queries and mutations against the API schema, and to +# generate python_anvil/types from it and the OpenAPI spec (dev/generate_types.py) graphql-core = ">=3.2" +# Pinned to a minor: `make check-generated` byte-compares its output +datamodel-code-generator = "~0.86" # So the CLI and its tests run from a dev install without --all-extras click = ">=8.0,<9" tabulate = ">=0.9,<0.11" @@ -98,6 +103,10 @@ ignore = [ # pytest-describe tests nest functions and shadow fixtures by design "tests/**" = ["D", "B018", "RUF012"] "examples/**" = ["D"] +# Generated from the API specs; their docstrings are the API's descriptions +"python_anvil/types/rest.py" = ["D", "UP", "RUF"] +"python_anvil/types/graphql.py" = ["D", "UP", "RUF"] +"python_anvil/types/operations.py" = ["D", "UP", "RUF"] [tool.ruff.lint.pydocstyle] convention = "pep257" diff --git a/python_anvil/api.py b/python_anvil/api.py index cc99a5f8..0916299a 100644 --- a/python_anvil/api.py +++ b/python_anvil/api.py @@ -4,6 +4,7 @@ import json as json_lib import logging +import os import warnings from collections.abc import Mapping from typing import Any, TypeVar @@ -66,7 +67,8 @@ class Anvil: Every method raises an `AnvilRequestError` when the API responds with an error, and a `requests` exception when a request can't be made at all. - :param api_key: Your API key, from your organization's settings. + :param api_key: Your API key, from your organization's settings. Read from + the `ANVIL_API_KEY` environment variable when not given. :param access_token: An OAuth access token, instead of an API key. If both are given, the access token is used. :param base_url: Where to send requests. Defaults to production, @@ -130,7 +132,12 @@ def __init__( stacklevel=2, ) if not (api_key or access_token): - raise ValueError("`api_key` or `access_token` is required") + # The way every example and the CLI find the key + api_key = os.environ.get("ANVIL_API_KEY") or None + if not (api_key or access_token): + raise ValueError( + "`api_key` or `access_token` is required, or set ANVIL_API_KEY" + ) self.client = HTTPClient( api_key, @@ -215,6 +222,7 @@ def fill_pdf( version_number: int | None = None, data_type: str = DATA_TYPE_BYTES, params: Mapping[str, Any] | None = None, + timeout: float | tuple[float, float] | None = None, ) -> Any: """Fill a PDF template with your data. Returns the PDF. @@ -226,6 +234,7 @@ def fill_pdf( recent version, published or not. :param data_type: `"bytes"` (the default) or `"stream"`. See `request_rest`. :param params: Extra query string parameters. + :param timeout: Overrides the client's timeout for this request. """ data = _parse_payload(payload, FillPDFPayload) _check_binary_data_type(data_type) @@ -240,6 +249,7 @@ def fill_pdf( json=data.model_dump(by_alias=True, exclude_none=True), params=query or None, data_type=data_type, + timeout=timeout, ) def generate_pdf( @@ -248,6 +258,7 @@ def generate_pdf( *, data_type: str = DATA_TYPE_BYTES, params: Mapping[str, Any] | None = None, + timeout: float | tuple[float, float] | None = None, ) -> Any: """Generate a PDF from Markdown or HTML. Returns the PDF. @@ -268,10 +279,15 @@ def generate_pdf( json=data.model_dump(by_alias=True, exclude_none=True), params=params, data_type=data_type, + timeout=timeout, ) def download_documents( - self, document_group_eid: str, *, data_type: str = DATA_TYPE_BYTES + self, + document_group_eid: str, + *, + data_type: str = DATA_TYPE_BYTES, + timeout: float | tuple[float, float] | None = None, ) -> Any: """Download the documents of an Etch packet's document group as a zip file. @@ -284,6 +300,7 @@ def download_documents( "GET", f"/api/document-group/{document_group_eid}.zip", data_type=data_type, + timeout=timeout, ) # GraphQL @@ -291,19 +308,25 @@ def download_documents( def request_graphql( self, query: str, - variables: Mapping[str, Any] | None = None, + variables: Mapping[str, Any] | str | bytes | None = None, *, timeout: float | tuple[float, float] | None = None, ) -> dict[str, Any]: """Send a GraphQL query or mutation. For operations without a method of their own. Returns the response's `data`. Files anywhere in `variables` are sent - as a multipart upload: see `prepare_graphql_file`. + as a multipart upload: see `prepare_graphql_file`. `variables` may be + a JSON string. See the GraphQL reference for every query and mutation: https://www.useanvil.com/docs/api/graphql/reference/ """ - operations = {"query": query, "variables": dict(variables or {})} + given = ( + json_lib.loads(variables) + if isinstance(variables, (str, bytes)) + else variables + ) + operations = {"query": query, "variables": dict(given or {})} # Checks for both file uploads and base64 uploads if not validate_upload_schema(operations["variables"]): @@ -330,25 +353,39 @@ def request_graphql( return body["data"] or {} def query( - self, query: str, variables: Mapping[str, Any] | None = None + self, + query: str, + variables: Mapping[str, Any] | str | bytes | None = None, + *, + timeout: float | tuple[float, float] | None = None, ) -> dict[str, Any]: """Send a GraphQL query. The same as `request_graphql`.""" - return self.request_graphql(query, variables) + return self.request_graphql(query, variables, timeout=timeout) def mutate( - self, query: str | BaseQuery, variables: Mapping[str, Any] | None = None + self, + query: str | BaseQuery, + variables: Mapping[str, Any] | str | bytes | None = None, + *, + timeout: float | tuple[float, float] | None = None, ) -> dict[str, Any]: """Send a GraphQL mutation, as a string or a mutation class. The same as `request_graphql`.""" mutation = query if isinstance(query, str) else query.get_mutation() - return self.request_graphql(mutation, variables) + return self.request_graphql(mutation, variables, timeout=timeout) def create_etch_packet( self, - payload: dict | CreateEtchPacketPayload | CreateEtchPacket | None = None, + payload: dict + | str + | bytes + | CreateEtchPacketPayload + | CreateEtchPacket + | None = None, json: str | bytes | None = None, *, response_query: str | None = None, mutation: str | None = None, + timeout: float | tuple[float, float] | None = None, ) -> dict[str, Any]: """Create an Etch e-sign packet, and send it to the first signer unless it's a draft. @@ -364,11 +401,13 @@ def create_etch_packet( :param mutation: Your own `createEtchPacket` mutation, with its response fields, instead of the client's. `response_query` is then ignored. """ - if payload is None and json is None: + if not payload and json is None: raise TypeError("One of the arguments `payload` or `json` must exist") if json is not None: payload = CreateEtchPacketPayload.model_validate_json(json) + elif isinstance(payload, (str, bytes)): + payload = CreateEtchPacketPayload.model_validate_json(payload) if isinstance(payload, dict): builder = CreateEtchPacket.create_from_dict(payload) @@ -385,11 +424,15 @@ def create_etch_packet( by_alias=True, exclude_none=True ) return self.request_graphql( - mutation or builder.get_mutation(response_query), variables + mutation or builder.get_mutation(response_query), variables, timeout=timeout ) def get_etch_packet( - self, eid: str, *, response_query: str | None = None + self, + eid: str, + *, + response_query: str | None = None, + timeout: float | tuple[float, float] | None = None, ) -> dict[str, Any]: """Get an Etch packet's details and status. Returns the packet. @@ -397,11 +440,19 @@ def get_etch_packet( :param response_query: The fields to select, instead of the defaults. """ data = self.request_graphql( - queries.etch_packet.generate_query(response_query), {"eid": eid} + queries.etch_packet.generate_query(response_query), + {"eid": eid}, + timeout=timeout, ) return data["etchPacket"] - def generate_etch_signing_url(self, signer_eid: str, client_user_id: str) -> str: + def generate_etch_signing_url( + self, + signer_eid: str, + client_user_id: str, + *, + timeout: float | tuple[float, float] | None = None, + ) -> str: """Generate the URL an embedded signer signs at. Returns the URL. The packet must have been sent, and the signer must be an `embedded` signer. @@ -413,7 +464,9 @@ def generate_etch_signing_url(self, signer_eid: str, client_user_id: str) -> str signer_eid=signer_eid, client_user_id=client_user_id ) data = self.request_graphql( - mutation.get_mutation(), mutation.create_payload().model_dump(by_alias=True) + mutation.get_mutation(), + mutation.create_payload().model_dump(by_alias=True), + timeout=timeout, ) return data["generateEtchSignURL"] @@ -421,11 +474,12 @@ def generate_etch_signing_url(self, signer_eid: str, client_user_id: str) -> str def forge_submit( self, - payload: dict | ForgeSubmitPayload | None = None, + payload: dict | str | bytes | ForgeSubmitPayload | None = None, json: str | bytes | None = None, *, response_query: str | None = None, mutation: str | None = None, + timeout: float | tuple[float, float] | None = None, ) -> dict[str, Any]: """Submit data to a webform (forge) in a workflow (weld). Returns the response's `data`. @@ -437,11 +491,13 @@ def forge_submit( of the defaults. :param mutation: Your own `forgeSubmit` mutation, instead of the client's. """ - if payload is None and json is None: + if not payload and json is None: raise TypeError("One of arguments `json` or `payload` are required") if json is not None: payload = ForgeSubmitPayload.model_validate_json(json) + elif isinstance(payload, (str, bytes)): + payload = ForgeSubmitPayload.model_validate_json(payload) if isinstance(payload, dict): builder = ForgeSubmit.create_from_dict(payload) @@ -456,17 +512,25 @@ def forge_submit( by_alias=True, exclude_none=True ) return self.request_graphql( - mutation or builder.get_mutation(response_query), variables + mutation or builder.get_mutation(response_query), variables, timeout=timeout ) - def remove_weld_data(self, eid: str, *, mutation: str | None = None) -> bool: + def remove_weld_data( + self, + eid: str, + *, + mutation: str | None = None, + timeout: float | tuple[float, float] | None = None, + ) -> bool: """Remove a workflow submission's data (a weld data). Returns whether it was removed. :param eid: The weld data's eid, from `forge_submit`'s `weldData`. :param mutation: Your own `removeWeldData` mutation, instead of the client's. """ data = self.request_graphql( - mutation or mutations.remove_weld_data.generate_mutation(), {"eid": eid} + mutation or mutations.remove_weld_data.generate_mutation(), + {"eid": eid}, + timeout=timeout, ) return bool(data["removeWeldData"]) diff --git a/python_anvil/api_resources/mutations/base.py b/python_anvil/api_resources/mutations/base.py index 0816358f..08881916 100644 --- a/python_anvil/api_resources/mutations/base.py +++ b/python_anvil/api_resources/mutations/base.py @@ -2,6 +2,8 @@ from typing import Any +from python_anvil.graphql.selection import selection + class BaseQuery: """Base class for the classes that build a GraphQL mutation and its variables.""" @@ -10,13 +12,21 @@ class BaseQuery: default_response_query: str | None = None # The same, set on a subclass or instance the way it was before 6.0.0 mutation_res_query: str | None = None + # A subclass's own mutation, as before 6.0.0: a template whose `{query}` + # is replaced with the response fields + mutation: str | None = None def get_mutation(self, response_query: str | None = None) -> str: """The mutation to send, selecting `response_query` or the default fields.""" - raise NotImplementedError + if self.mutation is None: + raise NotImplementedError + return self.mutation.format(query=self.response_fields(response_query) or "") def response_fields(self, response_query: str | None = None) -> str | None: - return response_query or self.mutation_res_query or self.default_response_query + """The selection to put after the mutation, in braces whether given with them or not.""" + return selection( + response_query or self.mutation_res_query or self.default_response_query + ) def create_payload(self) -> Any: """The model whose dump is the mutation's variables.""" diff --git a/python_anvil/api_resources/mutations/create_etch_packet.py b/python_anvil/api_resources/mutations/create_etch_packet.py index 63e048a2..227b347d 100644 --- a/python_anvil/api_resources/mutations/create_etch_packet.py +++ b/python_anvil/api_resources/mutations/create_etch_packet.py @@ -4,6 +4,8 @@ from mimetypes import guess_type from typing import Any +from pydantic import TypeAdapter + from python_anvil.api_resources.mutations.base import BaseQuery from python_anvil.api_resources.payload import ( AttachableEtchFile, @@ -16,11 +18,16 @@ DEFAULT_RESPONSE_QUERY, generate_mutation, ) -from python_anvil.utils import create_unique_id +from python_anvil.utils import camel_to_snake, create_unique_id logger: Logger = logging.getLogger(__name__) +_ATTACHABLE_FILE: TypeAdapter[AttachableEtchFile] = TypeAdapter(AttachableEtchFile) + +# The mutation, under the name it had before 6.0.0 +CREATE_ETCH_PACKET = generate_mutation() + class CreateEtchPacket(BaseQuery): """Builds a `createEtchPacket` mutation, adding signers and files one at a time. @@ -54,10 +61,7 @@ def __init__( ): # `name` is required when `payload` is not present. if not payload and not name: - raise TypeError( - "Missing 2 required positional arguments: 'name' and " - "'signature_email_subject'" - ) + raise TypeError("`name` is required") self.name = name self.signature_email_subject = signature_email_subject @@ -84,7 +88,11 @@ def get_mutation(self, response_query: str | None = None) -> str: @classmethod def create_from_dict(cls, payload: dict) -> "CreateEtchPacket": - """Create a new instance of `CreateEtchPacket` from a dict payload.""" + """Create a new instance of `CreateEtchPacket` from a dict payload. + + Keys may be snake_case or camelCase, like the mutation's variables. + """ + payload = {camel_to_snake(k): v for k, v in payload.items()} try: mutation = cls( **{k: v for k, v in payload.items() if k not in ["signers", "files"]} @@ -93,13 +101,11 @@ def create_from_dict(cls, payload: dict) -> "CreateEtchPacket": raise ValueError( f"`payload` must be a valid CreateEtchPacket instance or dict. {e}" ) from e - if "signers" in payload: - for signer in payload["signers"]: - mutation.add_signer(EtchSigner(**signer)) + for signer in payload.get("signers") or []: + mutation.add_signer(signer) - if "files" in payload: - for file in payload["files"]: - mutation.add_file(DocumentUpload(**file)) + for file in payload.get("files") or []: + mutation.add_file(file) return mutation @@ -116,11 +122,6 @@ def add_signer(self, signer: dict | EtchSigner): else: raise ValueError("Signer must be either a dict or EtchSigner type") - if data.signer_type not in ["embedded", "email"]: - raise ValueError( - "Etch signer `signer_type` must be only 'embedded' or 'email" - ) - if not data.id: data.id = create_unique_id("signer") if not data.routing_order: @@ -136,12 +137,16 @@ def add_signer(self, signer: dict | EtchSigner): self.signers.append(data) - def add_file(self, file: AttachableEtchFile): - """Add file to a pending list of Upload objects. + def add_file(self, file: AttachableEtchFile | dict): + """Add a file to a pending list of files. - Files will not be uploaded when running this method. They will be - uploaded when the mutation actually runs. + A `DocumentUpload`, `EtchCastRef`, `DocumentMarkdown` or `DocumentMarkup`, + or a dict of one of those, with snake_case or camelCase keys. Files + will not be uploaded when running this method. They will be uploaded + when the mutation actually runs. """ + if isinstance(file, dict): + file = _ATTACHABLE_FILE.validate_python(file) if ( isinstance(file, DocumentUpload) and isinstance(file.file, BufferedIOBase) @@ -188,7 +193,7 @@ def create_payload(self) -> CreateEtchPacketPayload: return self.payload if not self.name: - raise TypeError("`name` and `signature_email_subject` cannot be None") + raise TypeError("`name` is required") payload = CreateEtchPacketPayload( is_test=self.is_test, diff --git a/python_anvil/api_resources/mutations/forge_submit.py b/python_anvil/api_resources/mutations/forge_submit.py index dedf9e10..f2af0b09 100644 --- a/python_anvil/api_resources/mutations/forge_submit.py +++ b/python_anvil/api_resources/mutations/forge_submit.py @@ -7,6 +7,11 @@ DEFAULT_RESPONSE_QUERY, generate_mutation, ) +from python_anvil.utils import camel_to_snake + + +# The mutation, under the name it had before 6.0.0 +FORGE_SUBMIT = generate_mutation() class ForgeSubmit(BaseQuery): @@ -57,9 +62,8 @@ def __init__( @classmethod def create_from_dict(cls, payload: dict[str, Any]): - # Parse the data through the model class to validate and pass it back - # as variables in this class. - return cls(**payload) + """Create a `ForgeSubmit` from a dict. Keys may be snake_case or camelCase.""" + return cls(**{camel_to_snake(k): v for k, v in payload.items()}) def create_payload(self): # If provided a payload and no forge_eid, we'll assume that it's the diff --git a/python_anvil/api_resources/mutations/helpers.py b/python_anvil/api_resources/mutations/helpers.py index c72cbc90..3f68cf3e 100644 --- a/python_anvil/api_resources/mutations/helpers.py +++ b/python_anvil/api_resources/mutations/helpers.py @@ -1,4 +1,4 @@ -from python_anvil.api_resources.base import BaseModel +from pydantic import BaseModel def get_payload_attrs(payload_model: type[BaseModel]) -> list[str]: diff --git a/python_anvil/api_resources/payload.py b/python_anvil/api_resources/payload.py index 01b35121..803a1073 100644 --- a/python_anvil/api_resources/payload.py +++ b/python_anvil/api_resources/payload.py @@ -8,6 +8,7 @@ ) from python_anvil.models import FileCompatibleBaseModel +from python_anvil.types import operations, rest from .base import BaseModel @@ -18,11 +19,17 @@ class EmbeddedLogo(BaseModel): max_height: int | None = None -class FillPDFPayload(BaseModel): +class FillPDFPayload(rest.FillPdfRequest): + """The body of a fill PDF request: `data` and the API's styling options. + + See `python_anvil.types.rest.FillPdfRequest` for every option. + """ + data: list[dict[str, Any]] | dict[str, Any] - title: str | None = None - font_size: int | None = None - text_color: str | None = None + # As before 6.0.0, any size; the API says what it accepts + font_size: int | None = Field(None, alias="fontSize") + # The API defaults this to "pdf"; leave it out rather than always sending it + output_format: Literal["pdf", "url"] | None = Field(None, alias="outputFormat") @field_validator("data") @classmethod @@ -32,15 +39,20 @@ def data_cannot_be_empty(cls, v): return v -class GeneratePDFPayload(BaseModel): - data: list[dict[str, Any]] | dict[Literal["html", "css"], str] - logo: EmbeddedLogo | None = None - title: str | None = None +class GeneratePDFPayload(rest.GenerateMarkdownPdfRequest): + """The body of a generate PDF request, from Markdown (the default) or HTML. + + See `python_anvil.types.rest.GenerateMarkdownPdfRequest` and + `GenerateHtmlPdfRequest` for every option. + """ + + data: list[dict[str, Any]] | dict[Literal["html", "css"], str] # type: ignore[assignment] + logo: EmbeddedLogo | Literal[False] | None = None # type: ignore[assignment] type: Literal["markdown", "html"] | None = "markdown" - page: dict[str, Any] | None = None - font_size: int | None = None - font_family: str | None = None - text_color: str | None = None + # As before 6.0.0, any size; the API says what it accepts + font_size: int | None = Field(None, alias="fontSize") + # The API defaults this to "pdf"; leave it out rather than always sending it + output_format: Literal["pdf", "url"] | None = Field(None, alias="outputFormat") class GenerateEtchSigningURLPayload(BaseModel): @@ -53,21 +65,23 @@ class SignerField(BaseModel): field_id: str -class EtchSigner(BaseModel): - """Dataclass representing etch signers.""" +class EtchSigner(rest.EtchSigner): + """A signer of an Etch packet: who signs, which fields, and how. + + See `python_anvil.types.rest.EtchSigner` for every option. + """ name: str email: str - fields: list[SignerField] - signer_type: str = "email" + fields: list[SignerField] # type: ignore[assignment] + signer_type: Literal["email", "embedded", "in-person"] = Field( + "email", alias="signerType" + ) # id will be generated if `None` - id: str | None = None - routing_order: int | None = None - redirect_url: str | None = Field(None, alias="redirectURL") - accept_each_field: bool | None = None - enable_emails: list[str] | None = None - # signature_mode can be "draw" or "text" (default: text) - signature_mode: str | None = None + id: str | None = None # type: ignore[assignment] + # As before 6.0.0, any order; the API says what it accepts + routing_order: int | None = Field(None, alias="routingOrder") + enable_emails: bool | list[str] | None = Field(None, alias="enableEmails") # type: ignore[assignment] class SignatureField(BaseModel): @@ -189,60 +203,41 @@ class CreateEtchFilePayload(BaseModel): payloads: str | dict[str, FillPDFPayload] -class CreateEtchPacketPayload(FileCompatibleBaseModel): - """ - Payload for createEtchPacket. +class CreateEtchPacketPayload(FileCompatibleBaseModel, operations.CreateEtchPacketArgs): + """The variables of the `createEtchPacket` mutation. - See the full packet payload defined here: + See `python_anvil.types.operations.CreateEtchPacketArgs` for every + variable, and the full packet payload defined here: https://www.useanvil.com/docs/api/e-signatures#tying-it-all-together """ name: str - signers: list[EtchSigner] + signers: list[EtchSigner] # type: ignore[assignment] # NOTE: This is a list of `AttachableEtchFile` objects, but we need to # override the default `FileCompatibleBaseModel` to handle multipart/form-data # uploads correctly. This field name is referenced in the models.py file. - files: list["AttachableEtchFile"] - signature_email_subject: str | None = None - signature_email_body: str | None = None - is_draft: bool | None = False - is_test: bool | None = True - merge_pdfs: bool | None = Field(None, alias="mergePDFs") + files: list["AttachableEtchFile"] # type: ignore[assignment] + is_draft: bool | None = Field(False, alias="isDraft") + is_test: bool | None = Field(True, alias="isTest") data: CreateEtchFilePayload | None = None - signature_page_options: dict[Any, Any] | None = None - webhook_url: str | None = Field(None, alias="webhookURL") - reply_to_name: Any | None = None - reply_to_email: Any | None = None - enable_emails: bool | list[str] | None = None - create_cast_templates_from_uploads: bool | None = None - duplicate_casts: bool | None = None - organization_eid: str | None = None - allow_updates: bool | None = None - - -class ForgeSubmitPayload(BaseModel): - """ - Payload for forgeSubmit. + signature_page_options: dict[Any, Any] | None = Field( + None, alias="signaturePageOptions" + ) + enable_emails: bool | list[str] | None = Field(None, alias="enableEmails") + + +class ForgeSubmitPayload(operations.ForgeSubmitArgs): + """The variables of the `forgeSubmit` mutation. - See full payload defined here: + See `python_anvil.types.operations.ForgeSubmitArgs` for every variable, + and the full payload defined here: https://www.useanvil.com/docs/api/graphql/reference/#operation-forgesubmit-Mutations """ - forge_eid: str payload: dict[str, Any] - weld_data_eid: str | None = None - submission_eid: str | None = None - # Defaults to True when not provided/is None - enforce_payload_valid_on_create: bool | None = None - current_step: int | None = None - complete: bool | None = None # Note that if using a development API key, this will be forced to `True` # even when `False` is used in the payload. - is_test: bool | None = True - timezone: str | None = None - webhook_url: str | None = Field(None, alias="webhookURL") - group_array_id: str | None = None - group_array_index: int | None = None + is_test: bool | None = Field(True, alias="isTest") UploadableFile = Base64Upload | BufferedIOBase diff --git a/python_anvil/graphql/mutations/create_etch_packet.py b/python_anvil/graphql/mutations/create_etch_packet.py index 9aeb0a29..9dce7310 100644 --- a/python_anvil/graphql/mutations/create_etch_packet.py +++ b/python_anvil/graphql/mutations/create_etch_packet.py @@ -1,3 +1,6 @@ +from python_anvil.graphql.selection import selection + + DEFAULT_RESPONSE_QUERY = """{ eid name @@ -29,6 +32,7 @@ def generate_mutation(response_query: str | None = None) -> str: + response_query = selection(response_query) return f""" mutation CreateEtchPacket( $name: String, diff --git a/python_anvil/graphql/mutations/forge_submit.py b/python_anvil/graphql/mutations/forge_submit.py index 9ee8a60f..04d07243 100644 --- a/python_anvil/graphql/mutations/forge_submit.py +++ b/python_anvil/graphql/mutations/forge_submit.py @@ -1,3 +1,6 @@ +from python_anvil.graphql.selection import selection + + DEFAULT_RESPONSE_QUERY = """{ id eid @@ -24,6 +27,7 @@ def generate_mutation(response_query: str | None = None) -> str: + response_query = selection(response_query) return f""" mutation ForgeSubmit( $forgeEid: String!, diff --git a/python_anvil/graphql/queries/etch_packet.py b/python_anvil/graphql/queries/etch_packet.py index a8918f9b..e377e900 100644 --- a/python_anvil/graphql/queries/etch_packet.py +++ b/python_anvil/graphql/queries/etch_packet.py @@ -1,3 +1,6 @@ +from python_anvil.graphql.selection import selection + + DEFAULT_RESPONSE_QUERY = """{ id eid @@ -30,6 +33,7 @@ def generate_query(response_query: str | None = None) -> str: + response_query = selection(response_query) return f""" query EtchPacket ( $eid: String!, diff --git a/python_anvil/graphql/selection.py b/python_anvil/graphql/selection.py new file mode 100644 index 00000000..d29a085d --- /dev/null +++ b/python_anvil/graphql/selection.py @@ -0,0 +1,15 @@ +"""The response fields of an operation, with the braces a caller may leave out.""" + +from __future__ import annotations + + +def selection(response_query: str | None) -> str | None: + """`"{ eid name }"` for `"eid name"` or `"{ eid name }"`; `None` stays `None`.""" + if response_query is None: + return None + fields = response_query.strip() + if not fields: + return None + if fields.startswith("{"): + return fields + return f"{{ {fields} }}" diff --git a/python_anvil/http.py b/python_anvil/http.py index 9ec1b2ef..a6fc853f 100644 --- a/python_anvil/http.py +++ b/python_anvil/http.py @@ -66,6 +66,8 @@ def __init__( rate_limiter: RateLimiter | None = None, ): self.base_url = normalize_base_url(base_url) + self.api_key = api_key + self.access_token = access_token self.auth_header = build_auth_header(api_key, access_token) self.user_agent = user_agent or default_user_agent() self.timeout = timeout diff --git a/python_anvil/models.py b/python_anvil/models.py index 25f6f12f..e488753d 100644 --- a/python_anvil/models.py +++ b/python_anvil/models.py @@ -2,6 +2,7 @@ import os from io import BufferedReader, BytesIO from mimetypes import guess_type +from typing import Any from pydantic import BaseModel, ConfigDict @@ -39,16 +40,20 @@ def _check_if_serialization_iterator(self, value): value, "__next__" ) - def _process_file_data(self, file_obj): + def _process_file_data(self, file_obj, original=None): """Process file object into base64 encoded dict format.""" # Read the file data and encode it as base64 file_content = file_obj.read() - # Get filename - handle both regular files and BytesIO objects - filename = getattr(file_obj, "name", "document.pdf") - + # The name of the file as it was given; the copy read here has none + filename = ( + getattr(original, "name", None) + or getattr(file_obj, "name", None) + or "document.pdf" + ) if isinstance(filename, (bytes, bytearray)): filename = filename.decode("utf-8") + filename = str(filename) # manage mimetype based on file extension mimetype = guess_type(filename)[0] or "application/pdf" @@ -59,13 +64,21 @@ def _process_file_data(self, file_obj): "filename": os.path.basename(filename), } + def _original_file_item(self, index: int) -> Any: + files = getattr(self, "files", None) + if not isinstance(files, list) or index >= len(files): + return None + return files[index] + def model_dump(self, **kwargs): data = super().model_dump(**kwargs) for key, value in data.items(): if key == "file" and self._check_if_serialization_iterator(value): # Direct file case file_obj = self._iterator_to_buffered_reader(value) - data[key] = self._process_file_data(file_obj) + data[key] = self._process_file_data( + file_obj, getattr(self, "file", None) + ) elif key == "files" and isinstance(value, list): # List of objects case for index, item in enumerate(value): @@ -75,5 +88,10 @@ def model_dump(self, **kwargs): and self._check_if_serialization_iterator(item["file"]) ): file_obj = self._iterator_to_buffered_reader(item["file"]) - data[key][index]["file"] = self._process_file_data(file_obj) + original = getattr( + self._original_file_item(index), "file", None + ) + data[key][index]["file"] = self._process_file_data( + file_obj, original + ) return data diff --git a/python_anvil/types/__init__.py b/python_anvil/types/__init__.py new file mode 100644 index 00000000..50e81e6a --- /dev/null +++ b/python_anvil/types/__init__.py @@ -0,0 +1,58 @@ +"""Types generated from the Anvil API's OpenAPI spec and GraphQL schema. + +The Python counterpart of `@anvilco/types`: pydantic models, regenerated +from the API with `make update-schema generate`. + +- `python_anvil.types.rest`: the REST API's request bodies, like + `FillPdfRequest` and `GenerateMarkdownPdfRequest`. +- `python_anvil.types.graphql`: every type in the GraphQL schema, by name: + objects like `EtchPacket` and inputs like `EtchFile`. +- `python_anvil.types.operations`: the arguments of every query and + mutation, like `CreateEtchPacketArgs` and `ForgeSubmitArgs`. + +Field names are snake_case with camelCase aliases, and either works when +constructing a model. `model_dump(by_alias=True, exclude_none=True)` gives +the camelCase JSON the API wants. The client's own payload models in +`python_anvil.api_resources.payload` extend these. + +The named request models and the schema's types are stable names. The REST +module also has models for the spec's unnamed nested objects, like `Fields1` +and `Data2`, named by position; those can change when the spec does. +""" + +from python_anvil.types import graphql, operations, rest +from python_anvil.types._base import GeneratedModel +from python_anvil.types.operations import ( + CreateEtchPacketArgs, + EtchPacketArgs, + ForgeSubmitArgs, + GenerateEtchSignURLArgs, + RemoveWeldDataArgs, +) +from python_anvil.types.rest import ( + EtchFinishPageOptions, + EtchSignaturePageOptions, + FillPdfRequest, + GenerateHtmlPdfRequest, + GenerateMarkdownPdfRequest, + PageOptions, +) + + +__all__ = [ + "CreateEtchPacketArgs", + "EtchFinishPageOptions", + "EtchPacketArgs", + "EtchSignaturePageOptions", + "FillPdfRequest", + "ForgeSubmitArgs", + "GenerateEtchSignURLArgs", + "GenerateHtmlPdfRequest", + "GenerateMarkdownPdfRequest", + "GeneratedModel", + "PageOptions", + "RemoveWeldDataArgs", + "graphql", + "operations", + "rest", +] diff --git a/python_anvil/types/_base.py b/python_anvil/types/_base.py new file mode 100644 index 00000000..7892ec72 --- /dev/null +++ b/python_anvil/types/_base.py @@ -0,0 +1,21 @@ +"""The base class of the generated models.""" + +from pydantic import BaseModel, ConfigDict + + +class GeneratedModel(BaseModel): + """A generated model. + + Fields are snake_case with camelCase aliases, and can be set by either + name. Enum fields hold their plain values, so `model_dump` gives JSON. + Extra fields are kept, so a field the API added that these types don't + have yet still gets sent. + """ + + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + use_enum_values=True, + # The API may well have a field that starts with `model_` + protected_namespaces=(), + ) diff --git a/python_anvil/types/graphql.py b/python_anvil/types/graphql.py new file mode 100644 index 00000000..ec122b3d --- /dev/null +++ b/python_anvil/types/graphql.py @@ -0,0 +1,2626 @@ +# Generated by dev/generate_types.py from schema/anvil_schema.graphql. Do not edit by hand. +# Run `make generate` to regenerate. + +from __future__ import annotations + +from typing import Annotated, Any + +from pydantic import ConfigDict, Field +from typing_extensions import TypeAliasType + +from python_anvil.types._base import GeneratedModel + + +Boolean = TypeAliasType("Boolean", bool) +""" +The `Boolean` scalar type represents `true` or `false`. +""" + + +Date = TypeAliasType("Date", str) +""" +Datetime value represented in `ISO8601`. +""" + + +Float = TypeAliasType("Float", float) +""" +The `Float` scalar type represents signed double-precision fractional values as specified by [IEEE 754](https://en.wikipedia.org/wiki/IEEE_floating_point). +""" + + +ID = TypeAliasType("ID", str) +""" +The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `"4"`) or integer (such as `4`) input value will be accepted as an ID. +""" + + +Int = TypeAliasType("Int", int) +""" +The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. +""" + + +String = TypeAliasType("String", str) +""" +The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. +""" + + +class Amount(GeneratedModel): + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + amount: Annotated[Float, Field(description="Monetary amount value")] + currency: Annotated[String, Field(description="Currency code (e.g., USD)")] + + +class BillingPeriod(GeneratedModel): + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + end: Annotated[ + String, + Field( + description="End date (exclusive) of the billing period in ISO 8601 format; from Metronome this is ending_before (the first instant outside the billing period)" + ), + ] + start: Annotated[ + String, + Field( + description="Start date (inclusive) of the billing period in ISO 8601 format" + ), + ] + + +class BillingSubscription(GeneratedModel): + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + attributes: Annotated[ + Any, Field(description="Arbitrary subscription attributes") + ] = None + billing_interval: Annotated[ + String | None, + Field(alias="billingInterval", description="monthly | annual | null"), + ] = None + id: Annotated[ID, Field(description="Subscription identifier")] + name: Annotated[String, Field(description="Subscription name")] + price: Annotated[ + Amount | None, Field(description="Amount for the subscription") + ] = None + status: Annotated[String, Field(description="active | available | expired")] + + +class CastRedirectMapping(GeneratedModel): + """ + A mapping from an old redirect path to its canonical destination + """ + + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + cast_eid: Annotated[ + String, + Field(alias="castEid", description="The Cast EID this redirect belongs to"), + ] + from_: Annotated[ + String, + Field( + alias="from", + description="The old redirect path (e.g., /forms/old-tag/old-slug/)", + ), + ] + to: Annotated[ + String, + Field( + description="The canonical destination path (e.g., /forms/current-tag/current-slug/)" + ), + ] + + +class File(GeneratedModel): + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + encoding: String + key: String + mimetype: String + name: String + src: String + + +class GenerateEmbedURLResponse(GeneratedModel): + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + request_token_eid: Annotated[ + String, + Field( + alias="requestTokenEid", + description="The EID for the token associated with the request you just made. Can be used later to expire the session that was created using it.", + ), + ] + url: Annotated[ + String, + Field( + description="The URL to send your embedded User to. They will be logged in and then redirected to the appropriate page for their embedded access.\nYou can direct them back to this URL if they need to access their embedded experience again as long as their session has not expired." + ), + ] + + +class Integration(GeneratedModel): + """ + A third-party integration connection (e.g. HubSpot) for an organization. + """ + + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + is_connected: Annotated[ + Boolean, + Field( + alias="isConnected", + description="Whether the organization has an active connection for this integration.", + ), + ] + object_types: Annotated[ + Any, + Field( + alias="objectTypes", + description="Available object types, normalized into the `{ k, v }` shape expected by Dropdown.", + ), + ] = None + + +class Invoice(GeneratedModel): + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + end_date: Annotated[Date | None, Field(alias="endDate")] = None + id: String | None = None + invoice_url: Annotated[String | None, Field(alias="invoiceUrl")] = None + start_date: Annotated[Date | None, Field(alias="startDate")] = None + status: String | None = None + total_amount: Annotated[Float | None, Field(alias="totalAmount")] = None + + +class InvoiceBucketItem(GeneratedModel): + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + included_units: Annotated[Int | None, Field(alias="includedUnits")] = None + unit_type: Annotated[String | None, Field(alias="unitType")] = None + used_units: Annotated[Int | None, Field(alias="usedUnits")] = None + + +class InvoiceUnitItem(GeneratedModel): + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + base_units: Annotated[Int | None, Field(alias="baseUnits")] = None + total_amount: Annotated[Float | None, Field(alias="totalAmount")] = None + unit_amount: Annotated[Float | None, Field(alias="unitAmount")] = None + unit_type: Annotated[String | None, Field(alias="unitType")] = None + used_units: Annotated[Int | None, Field(alias="usedUnits")] = None + + +class MetricUsage(GeneratedModel): + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + billable_usage: Annotated[ + Int, + Field( + alias="billableUsage", + description="Usage that will be billed (usage not covered by credits or prepaid)", + ), + ] + cost: Annotated[ + Amount, Field(description="Cost for billable usage (billableUsage * unitPrice)") + ] + credits_initial: Annotated[ + Int | None, + Field( + alias="creditsInitial", + description="Initial number of free credits granted for this metric", + ), + ] = None + credits_remaining: Annotated[ + Int | None, + Field( + alias="creditsRemaining", + description="Number of free credits remaining for this metric (null if no credits)", + ), + ] = None + key: Annotated[ + ID, Field(description="Metric key (e.g., pdfApiCalls, workflowSubmissions)") + ] + label: Annotated[String, Field(description="Display label for the metric")] + prepaid_initial: Annotated[ + Int | None, + Field( + alias="prepaidInitial", + description="Initial number of prepaid units from bulk purchases", + ), + ] = None + prepaid_remaining: Annotated[ + Int | None, + Field( + alias="prepaidRemaining", + description="Number of prepaid units remaining from bulk purchases (null if no prepaid)", + ), + ] = None + unit: Annotated[ + String, Field(description="Unit label (e.g., calls, submissions, packets)") + ] + unit_price: Annotated[ + Amount, Field(alias="unitPrice", description="Price per unit") + ] + + +class ObjectRelationship(GeneratedModel): + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + id: Int + related_object_eid: Annotated[String, Field(alias="relatedObjectEid")] + related_object_type: Annotated[String | None, Field(alias="relatedObjectType")] = ( + None + ) + relation: String | None = None + source_object_eid: Annotated[String, Field(alias="sourceObjectEid")] + source_object_type: Annotated[String | None, Field(alias="sourceObjectType")] = None + + +class PaymentMethod(GeneratedModel): + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + exp_month: Annotated[Int | None, Field(alias="expMonth")] = None + exp_year: Annotated[Int | None, Field(alias="expYear")] = None + id: String + is_default: Annotated[Boolean | None, Field(alias="isDefault")] = None + last4: String | None = None + provider_name: Annotated[String | None, Field(alias="providerName")] = None + status: String | None = None + type: String | None = None + + +class UserPreferences(GeneratedModel): + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + accepted_signature: Annotated[Boolean | None, Field(alias="acceptedSignature")] = ( + None + ) + initials: String | None = None + mcp_organization_eid: Annotated[ + String | None, Field(alias="mcpOrganizationEid") + ] = None + name: String | None = None + require2_fa: Annotated[Boolean | None, Field(alias="require2FA")] = None + + +class EtchFile(GeneratedModel): + """ + An object used by `createEtchPacket` to specify a file. See the + [Etch e-sign guide](/docs/api/e-signatures) for more info. + """ + + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + base64_file: Annotated[Any, Field(alias="base64File")] = None + cast_eid: Annotated[String | None, Field(alias="castEid")] = None + fields: list[Any] | None = None + file: Any = None + filename: Annotated[ + String | None, Field(description="Filename set when creating the `EtchPacket`.") + ] = None + font_size: Annotated[Int | None, Field(alias="fontSize")] = None + id: Annotated[ + String, Field(description="An id string set when creating the `EtchPacket`.") + ] + include_timestamp: Annotated[ + Boolean | None, + Field( + alias="includeTimestamp", + description='For generated PDFs, set to false to disable the "Generated at XXX" timestamp at the bottom of the document.', + ), + ] = None + logo: Annotated[ + Any, + Field( + description="For generated PDFs, you can specify a logo to be rendered at the top of the document." + ), + ] = None + markup: Annotated[Any, Field(description="Holds HTML/CSS to generate PDF")] = None + page: Annotated[ + Any, Field(description="Holds page customizations to generate PDF") + ] = None + page_sizes: Annotated[list[Any] | None, Field(alias="pageSizes")] = None + text_color: Annotated[String | None, Field(alias="textColor")] = None + title: String | None = None + + +class GenerateExpressionDataInput(GeneratedModel): + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + is_boolean: Annotated[ + Boolean | None, + Field( + alias="isBoolean", + description="A `Boolean` indicating whether the resulting expression shoudl resolve to a boolean value.", + ), + ] = False + user_statement: Annotated[ + String, + Field( + alias="userStatement", + description="A human-readable statement representing the expression you are trying to create. References to fields must be wrapped in double-brackets like so: `{{:}}`", + ), + ] + + +class WebhookActionConfigInput(GeneratedModel): + """ + Configuration options for a [`Webhook Action`]({{Types.WebhookAction}}). + """ + + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + do_on_staging: Annotated[ + Boolean | None, + Field( + alias="doOnStaging", + description="Whether actually make the webhook call on Staging", + ), + ] = None + encrypt_data: Annotated[ + Boolean | None, + Field( + alias="encryptData", + description="Whether to encrypt the data payloads delivered by the [`Webhook`]({{Types.Webhook}}) call. Defaults to `false`.", + ), + ] = False + + +class WeldFile(GeneratedModel): + """ + An object used by `createWeld` to specify a file. See the + [GraphQL reference](/docs/api/graphql/reference/#operation-createweld-Mutations) for more info. + """ + + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + cast_eid: Annotated[String | None, Field(alias="castEid")] = None + file: Any = None + id: Annotated[ + String, Field(description="An id string set when creating the `Weld`.") + ] + mimetype: String | None = None + name: String | None = None + src: String | None = None + stamp: Float | None = None + title: String | None = None + + +class Cast(GeneratedModel): + """ + A `Cast` holds configuration for a single PDF template. It defines the + location of boxes on PDF pages, and the type of each box (e.g. date, phone + number, etc.). + + `Cast` objects are used by several API features: + + * The [PDF filling endpoint](/docs/api/fill-pdf) + * Etch signature packets: see the [Etch e-sign guide](/docs/api/e-signatures) for more info + * Workflows: see the [Workflow API guide](/docs/api/workflows) for more info + """ + + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + allowed_alias_ids: Annotated[ + list[String | None] | None, + Field(alias="allowedAliasIds", description="Allowed `Cast` field alias IDs"), + ] = None + archived_at: Annotated[Date | None, Field(alias="archivedAt")] = None + config: Any = None + created_at: Annotated[Date | None, Field(alias="createdAt")] = None + eid: Annotated[ + String | None, + Field( + description="The `eid` is the Cast's object identifier. It is also the PDF template ID." + ), + ] = None + example_data: Annotated[ + Any, + Field( + alias="exampleData", + description='Example data payload to fill this PDF. Shown on the "API Info" tab for this template', + ), + ] = None + field_info: Annotated[ + Any, + Field( + alias="fieldInfo", + description="A digestable array of objects with all the details about all fields in this PDF, sorted by page, top to bottom, left to right.", + ), + ] = None + has_been_published: Annotated[Boolean | None, Field(alias="hasBeenPublished")] = ( + None + ) + has_unpublished_changes: Annotated[ + Boolean | None, Field(alias="hasUnpublishedChanges") + ] = None + is_latest_version: Annotated[Boolean | None, Field(alias="isLatestVersion")] = None + is_published_version: Annotated[ + Boolean | None, Field(alias="isPublishedVersion") + ] = None + is_template: Annotated[ + Boolean | None, + Field( + alias="isTemplate", + description="`true` when this `Cast` was created as a template. Template casts show up in the `PDF Templates` page on your dashboard.", + ), + ] = None + latest_draft_version_number: Annotated[ + Int | None, Field(alias="latestDraftVersionNumber") + ] = None + latest_published_version_info: Annotated[ + Any, + Field( + alias="latestPublishedVersionInfo", + description="Basic information about the latest published version (title and publisher name)", + ), + ] = None + name: Annotated[String | None, Field(description="The file name")] = None + organization: Organization | None = None + parent_cast: Annotated[ + Cast | None, + Field( + alias="parentCast", + description="If this `Cast` was originally copied from another `Cast`, this will be the original `Cast`.", + ), + ] = None + published_at: Annotated[Date | None, Field(alias="publishedAt")] = None + published_number: Annotated[ + Int | None, + Field( + alias="publishedNumber", + description="Current published version number of this Cast", + ), + ] = None + published_version_id: Annotated[Int | None, Field(alias="publishedVersionId")] = ( + None + ) + published_version_number: Annotated[ + Int | None, Field(alias="publishedVersionNumber") + ] = None + published_versions: Annotated[ + CastVersionPage | None, Field(alias="publishedVersions") + ] = None + title: Annotated[String | None, Field(description="The title shown in the UI")] = ( + None + ) + type: Annotated[ + String | None, Field(description="The file's mimetype. e.g. `application/pdf`") + ] = None + updated_at: Annotated[Date | None, Field(alias="updatedAt")] = None + user_uploads: Annotated[ + list[UserUpload | None] | None, + Field( + alias="userUploads", + description="An array of files uploaded by users during the dynamic template creation process.", + ), + ] = None + version_id: Annotated[Int | None, Field(alias="versionId")] = None + version_number: Annotated[ + Int | None, + Field(alias="versionNumber", description="Current version number of this Cast"), + ] = None + versions: CastVersionPage | None = None + + +class CastPage(GeneratedModel): + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + items: Annotated[ + list[Cast | None] | None, + Field(description="The actual records for the requested page"), + ] = None + page: Annotated[Int | None, Field(description="The requested page number")] = None + page_count: Annotated[ + Int | None, + Field(alias="pageCount", description="Total number of pages of results"), + ] = None + page_size: Annotated[ + Int | None, + Field(alias="pageSize", description="The requested number of rows per page"), + ] = None + row_count: Annotated[ + Int | None, + Field( + alias="rowCount", + description="Total number of rows found for the query before pagination. Capped when the query was given a `countLimit`.", + ), + ] = None + row_count_is_exact: Annotated[ + Boolean | None, + Field( + alias="rowCountIsExact", + description="False when `rowCount` stopped at the requested `countLimit`, meaning there are at least that many rows. True whenever the count ran to completion.", + ), + ] = None + + +class CastTag(GeneratedModel): + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + cast: Cast | None = None + created_at: Annotated[Date | None, Field(alias="createdAt")] = None + id: Int + tag: Tag | None = None + updated_at: Annotated[Date | None, Field(alias="updatedAt")] = None + + +class CastVersion(GeneratedModel): + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + config: Any = None + created_at: Annotated[Date | None, Field(alias="createdAt")] = None + eid: Annotated[String, Field(description="The `eid` is the object identifier.")] + id: Int + location: String | None = None + name: String | None = None + number: Int | None = None + publish_updated_at: Annotated[Date | None, Field(alias="publishUpdatedAt")] = None + published_at: Annotated[Date | None, Field(alias="publishedAt")] = None + published_description: Annotated[ + String | None, Field(alias="publishedDescription") + ] = None + published_number: Annotated[Int | None, Field(alias="publishedNumber")] = None + published_title: Annotated[String | None, Field(alias="publishedTitle")] = None + publishing_user: Annotated[User | None, Field(alias="publishingUser")] = None + title: String | None = None + type: String | None = None + updated_at: Annotated[Date | None, Field(alias="updatedAt")] = None + updated_columns: Annotated[ + list[String | None] | None, Field(alias="updatedColumns") + ] = None + user: User | None = None + + +class CastVersionPage(GeneratedModel): + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + items: Annotated[ + list[CastVersion | None] | None, + Field(description="The actual records for the requested page"), + ] = None + page: Annotated[Int | None, Field(description="The requested page number")] = None + page_count: Annotated[ + Int | None, + Field(alias="pageCount", description="Total number of pages of results"), + ] = None + page_size: Annotated[ + Int | None, + Field(alias="pageSize", description="The requested number of rows per page"), + ] = None + row_count: Annotated[ + Int | None, + Field( + alias="rowCount", + description="Total number of rows found for the query before pagination. Capped when the query was given a `countLimit`.", + ), + ] = None + row_count_is_exact: Annotated[ + Boolean | None, + Field( + alias="rowCountIsExact", + description="False when `rowCount` stopped at the requested `countLimit`, meaning there are at least that many rows. True whenever the count ran to completion.", + ), + ] = None + + +class DocumentGroup(GeneratedModel): + """ + A collection of all the final files generated with data from a `WeldData` or + `EtchPacket`. When your users download a zip file of PDFs, that final + collection of filled, generated, uploaded, and signed files is specified by + the `DocumentGroup`. A `DocumentGroup` will be connected to either a + `WeldData` or a `EtchPacket`, never both. A `DocumentGroup` will only + exist on these objects if it has been sent out for signature. + """ + + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + completed_at: Annotated[ + Date | None, + Field( + alias="completedAt", + description="Timestamp of when all Signers have finished signing or when all forms have been filled when no signers.", + ), + ] = None + created_at: Annotated[Date | None, Field(alias="createdAt")] = None + current_routing_step: Annotated[Int | None, Field(alias="currentRoutingStep")] = ( + None + ) + download_zip_url: Annotated[ + String | None, + Field( + alias="downloadZipURL", + description="The URL to download all documents as a zip file", + ), + ] = None + eid: Annotated[ + String | None, Field(description="The `eid` is the object identifier.") + ] = None + etch_packet: Annotated[ + EtchPacket | None, + Field( + alias="etchPacket", + description="The parent `EtchPacket`. This will only exist for DocumentGroups created as part of an Etch signature packet.", + ), + ] = None + files: Annotated[ + list[Any] | None, + Field(description="A file listing of all files in the `DocumentGroup`"), + ] = None + finish_page_config: Annotated[Any, Field(alias="finishPageConfig")] = None + org_logo: Annotated[Any, Field(alias="orgLogo")] = None + provider: Annotated[ + String | None, Field(description="Possible values: `etch` or `docusign`") + ] = None + provider_config: Annotated[Any, Field(alias="providerConfig")] = None + signature_page_config: Annotated[Any, Field(alias="signaturePageConfig")] = None + signers: list[Signer | None] | None = None + status: Annotated[ + String | None, + Field( + description="Possible values: `sent`, `delivered`, `partial`, `declined`, `voided`, `completed`" + ), + ] = None + stylesheet_url: Annotated[String | None, Field(alias="stylesheetURL")] = None + updated_at: Annotated[Date | None, Field(alias="updatedAt")] = None + weld_data: Annotated[ + WeldData | None, + Field( + alias="weldData", + description="The parent `WeldData`. This will only exist for DocumentGroups created by a Workflow.", + ), + ] = None + + +class DocumentGroupPage(GeneratedModel): + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + items: Annotated[ + list[DocumentGroup | None] | None, + Field(description="The actual records for the requested page"), + ] = None + page: Annotated[Int | None, Field(description="The requested page number")] = None + page_count: Annotated[ + Int | None, + Field(alias="pageCount", description="Total number of pages of results"), + ] = None + page_size: Annotated[ + Int | None, + Field(alias="pageSize", description="The requested number of rows per page"), + ] = None + row_count: Annotated[ + Int | None, + Field( + alias="rowCount", + description="Total number of rows found for the query before pagination. Capped when the query was given a `countLimit`.", + ), + ] = None + row_count_is_exact: Annotated[ + Boolean | None, + Field( + alias="rowCountIsExact", + description="False when `rowCount` stopped at the requested `countLimit`, meaning there are at least that many rows. True whenever the count ran to completion.", + ), + ] = None + + +class EtchLog(GeneratedModel): + """ + Represents a single action taken by a user during the signing process. For + example, an `EtchLog` object will be created when a user signs, downloads + a file, voids the packet, etc. + + All actions with a short description: + + ```md + accessed: Viewed + acknowledged: Reviewed & acknowledged + formCompleted: Filled & completed + archived: Archived + completed: Completed + created: Created + download-csv-all: Downloaded CSV + download-csv-individual-file: Downloaded CSV file + download-file-individual: Downloaded file + download-files-zip: Downloaded zip + emailed-completed: Emailed + emailed-by-user: Emailed sign link + emailed: Emailed + locked: Locked + pin-code-changed: Changed PIN + pin-code-failed: Access failed + pin-code-set: Set PIN + pin-code-unset: Unset PIN + pin-code-verified: Access verified + reassigned-signer: Updated signer details + external-update-signer: Updated signer details + signed: Signed + unarchived: Unarchived + unlocked: Unlocked + voided: Voided + saved-form: Saved form + upload-file-accessed: Accessed file + upload-file: Uploaded + ``` + """ + + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + action: Annotated[String | None, Field(description="The action taken.")] = None + created_at: Annotated[Date | None, Field(alias="createdAt")] = None + eid: Annotated[ + String | None, Field(description="The `eid` is the object identifier.") + ] = None + file_id: Annotated[ + String | None, + Field( + alias="fileId", + description="String id, often a filename, of any file related to the action taken.", + ), + ] = None + formatted_location: Annotated[ + String | None, + Field( + alias="formattedLocation", + description="Location of the user taking the action.", + ), + ] = None + id: Int + metadata: Annotated[Any, Field(description="Any action-specific information.")] = ( + None + ) + signer: Annotated[ + Signer | None, + Field(description="The Signer who took the action, if applicable."), + ] = None + user: Annotated[ + User | None, + Field(description="The Anvil `User` who took this action, if applicable."), + ] = None + user_location: Annotated[ + Any, + Field( + alias="userLocation", + description="The location of the user who took the action.", + ), + ] = None + + +class EtchPacket(GeneratedModel): + """ + An `EtchPacket` represents a signature packet. `EtchPackets` packets allow you to gather signatures from multiple signers on a collection of PDFs filled with your data (see `payload`), and uploads such as images. See the [e-signature guide](/docs/api/e-signatures) for usage. + """ + + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + allow_updates: Annotated[ + Boolean | None, + Field( + alias="allowUpdates", + description="When enabled, the EtchPacket payload can be updated. Requires a plan that allows EtchPacket updates.", + ), + ] = None + archived_at: Annotated[Date | None, Field(alias="archivedAt")] = None + completed_at: Annotated[Date | None, Field(alias="completedAt")] = None + contains_fill_data: Annotated[ + Boolean | None, + Field( + alias="containsFillData", + description="`true` when `payload` contains data that will fill PDFs", + ), + ] = None + created_at: Annotated[Date | None, Field(alias="createdAt")] = None + details_url: Annotated[ + String | None, + Field( + alias="detailsURL", + description="URL to this `EtchPacket` in the dashboard. Any users in your organization can view this URL.", + ), + ] = None + document_group: Annotated[ + DocumentGroup | None, + Field( + alias="documentGroup", + description="A `DocumentGroup` contains the documents after they have been filled and sent out for signature. The `DocumentGroup` will only exist here when `status != 'draft'`, i.e when documents have been sent out for signature. See the `DocumentGroup`'s `status` for lifecycle information.", + ), + ] = None + eid: Annotated[ + String | None, Field(description="The `eid` is the object identifier.") + ] = None + embedded_at: Annotated[Date | None, Field(alias="embeddedAt")] = None + etch_log_events: Annotated[ + list[EtchLog | None] | None, + Field( + alias="etchLogEvents", + description="An array of actions during the signing process. Details actions when files are signed, downloaded, etc.", + ), + ] = None + etch_template: Annotated[ + EtchTemplate | None, + Field( + alias="etchTemplate", + description="The `EtchTemplate` holds configuration for signers and files in the packet. Each `EtchPacket` will have an `EtchTemplate`.", + ), + ] = None + id: Int + is_free: Annotated[Boolean | None, Field(alias="isFree")] = None + is_test: Annotated[ + Boolean | None, + Field( + alias="isTest", + description="`true` when it is a test packet. You will not be charged for test packets. Filled and signed files will be watermarked.", + ), + ] = None + name: Annotated[String | None, Field(description="The name shown in the UI.")] = ( + None + ) + number_remaining_signers: Annotated[ + Int | None, Field(alias="numberRemainingSigners") + ] = None + organization: Organization | None = None + payload: Annotated[ + Any, Field(description="Data specified to fill PDFs in the packet.") + ] = None + sent_at: Annotated[Date | None, Field(alias="sentAt")] = None + status: Annotated[ + String | None, + Field( + description="Possible values: `draft`, `sent`, `delivered`, `partial`, `declined`, `voided`, `completed`." + ), + ] = None + updated_at: Annotated[Date | None, Field(alias="updatedAt")] = None + user_uploads: Annotated[ + list[UserUpload | None] | None, + Field( + alias="userUploads", + description="Uploaded files like images included in the packet.", + ), + ] = None + webhook_logs: Annotated[ + list[WebhookLog | None] | None, + Field( + alias="webhookLogs", + description="An array of webhook notification calls to your server related to this `EtchPacket`. Get the status codes, response times, and retry information from each `WebhookLog`.", + ), + ] = None + webhook_url: Annotated[ + String | None, + Field( + alias="webhookURL", + description="The specific webhook notification URL for this `EtchPacket`. We will POST to this URL when users take various actions.", + ), + ] = None + + +class EtchPacketPage(GeneratedModel): + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + items: Annotated[ + list[EtchPacket | None] | None, + Field(description="The actual records for the requested page"), + ] = None + page: Annotated[Int | None, Field(description="The requested page number")] = None + page_count: Annotated[ + Int | None, + Field(alias="pageCount", description="Total number of pages of results"), + ] = None + page_size: Annotated[ + Int | None, + Field(alias="pageSize", description="The requested number of rows per page"), + ] = None + row_count: Annotated[ + Int | None, + Field( + alias="rowCount", + description="Total number of rows found for the query before pagination. Capped when the query was given a `countLimit`.", + ), + ] = None + row_count_is_exact: Annotated[ + Boolean | None, + Field( + alias="rowCountIsExact", + description="False when `rowCount` stopped at the requested `countLimit`, meaning there are at least that many rows. True whenever the count ran to completion.", + ), + ] = None + + +class EtchTemplate(GeneratedModel): + """ + An `EtchTemplate` holds configuration for a signature packet. You should not need to interact directly with an `EtchTemplate` very much. See the [e-signature guide](/docs/api/e-signatures) for more info on sending documents out for signatures. + """ + + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + casts: Annotated[ + list[Cast | None] | None, + Field( + description="All `Cast` objects (fillable pdfs) included in this template." + ), + ] = None + config: Any = None + created_at: Annotated[Date | None, Field(alias="createdAt")] = None + eid: Annotated[ + String | None, Field(description="The `eid` is the object identifier.") + ] = None + etch_packets: Annotated[ + list[EtchPacket | None] | None, + Field( + alias="etchPackets", + description="An array of `EtchPackets` created from this template. At this time, this will be a 1-to-1 relationship.", + ), + ] = None + id: Int + is_free: Annotated[Boolean | None, Field(alias="isFree")] = None + is_repeatable: Annotated[Boolean | None, Field(alias="isRepeatable")] = None + name: String | None = None + organization: Organization | None = None + signers: Annotated[ + list[Any] | None, + Field( + description="Signer configurations including the inferred signatureExperience. Useful for inspecting signer state on draft packets before sending." + ), + ] = None + updated_at: Annotated[Date | None, Field(alias="updatedAt")] = None + user_uploads: Annotated[ + list[UserUpload | None] | None, Field(alias="userUploads") + ] = None + + +class Forge(GeneratedModel): + """ + A `Forge` holds configuration for a single webform. A `Forge` defines a + webform's fields, the page fields are on, their types, webform logic, etc. + Each Forge will be a member of only one `Weld` (Workflow). + + See our [Workflow API guide](/docs/api/workflows) for more info. + """ + + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + archived_at: Annotated[Date | None, Field(alias="archivedAt")] = None + config: Any = None + created_at: Annotated[Date | None, Field(alias="createdAt")] = None + eid: Annotated[ + String | None, + Field(description="The `eid` is the Forge/Webform's object identifier."), + ] = None + example_payload: Annotated[ + Any, + Field( + alias="examplePayload", + description='Example payload to fill this webform. Shown on the "API information" page for this webform\'s Workflow.', + ), + ] = None + field_info: Annotated[ + Any, + Field( + alias="fieldInfo", + description="A list of objects describing all the fields in this webform. The response is a more digestable way to get all the fields than digging in `config`.", + ), + ] = None + has_been_published: Annotated[Boolean | None, Field(alias="hasBeenPublished")] = ( + None + ) + has_unpublished_changes: Annotated[ + Boolean | None, Field(alias="hasUnpublishedChanges") + ] = None + id: Int + is_latest_version: Annotated[Boolean | None, Field(alias="isLatestVersion")] = None + is_published_version: Annotated[ + Boolean | None, Field(alias="isPublishedVersion") + ] = None + is_required: Annotated[ + Boolean | None, + Field( + alias="isRequired", + description="Is this forge required to complete the Workflow?", + ), + ] = None + latest_draft_version_number: Annotated[ + Int | None, Field(alias="latestDraftVersionNumber") + ] = None + name: String | None = None + organization: Organization | None = None + published_at: Annotated[Date | None, Field(alias="publishedAt")] = None + published_number: Annotated[ + Int | None, + Field( + alias="publishedNumber", + description="Current published version number of this Forge", + ), + ] = None + published_slug: Annotated[ + String | None, + Field( + alias="publishedSlug", + description="The slug of the currently published version", + ), + ] = None + published_version_id: Annotated[Int | None, Field(alias="publishedVersionId")] = ( + None + ) + published_version_number: Annotated[ + Int | None, Field(alias="publishedVersionNumber") + ] = None + published_versions: Annotated[ + ForgeVersionPage | None, Field(alias="publishedVersions") + ] = None + resolved_config: Annotated[ + Any, + Field( + alias="resolvedConfig", + description="The forge `config` with cascade-resolved flags merged in (e.g. `optionalByDefault`, resolved forge → weld → org). Read this to render/validate the form; raw `config` is for editing the stored config.", + ), + ] = None + slug: String | None = None + test_forge_complete_webhook_payload: Annotated[ + Any, Field(alias="testForgeCompleteWebhookPayload") + ] = None + updated_at: Annotated[Date | None, Field(alias="updatedAt")] = None + user_uploads: Annotated[ + list[UserUpload | None] | None, Field(alias="userUploads") + ] = None + version_id: Annotated[Int | None, Field(alias="versionId")] = None + version_number: Annotated[ + Int | None, + Field( + alias="versionNumber", description="Current version number of this Forge" + ), + ] = None + versions: ForgeVersionPage | None = None + weld: Weld | None = None + + +class ForgeMap(GeneratedModel): + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + config: Any = None + created_at: Annotated[Date | None, Field(alias="createdAt")] = None + eid: Annotated[ + String | None, Field(description="The `eid` is the object identifier.") + ] = None + forge_a: Annotated[Forge | None, Field(alias="forgeA")] = None + forge_b: Annotated[Forge | None, Field(alias="forgeB")] = None + id: Int + updated_at: Annotated[Date | None, Field(alias="updatedAt")] = None + + +class ForgePage(GeneratedModel): + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + items: Annotated[ + list[Forge | None] | None, + Field(description="The actual records for the requested page"), + ] = None + page: Annotated[Int | None, Field(description="The requested page number")] = None + page_count: Annotated[ + Int | None, + Field(alias="pageCount", description="Total number of pages of results"), + ] = None + page_size: Annotated[ + Int | None, + Field(alias="pageSize", description="The requested number of rows per page"), + ] = None + row_count: Annotated[ + Int | None, + Field( + alias="rowCount", + description="Total number of rows found for the query before pagination. Capped when the query was given a `countLimit`.", + ), + ] = None + row_count_is_exact: Annotated[ + Boolean | None, + Field( + alias="rowCountIsExact", + description="False when `rowCount` stopped at the requested `countLimit`, meaning there are at least that many rows. True whenever the count ran to completion.", + ), + ] = None + + +class ForgeVersion(GeneratedModel): + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + config: Any = None + created_at: Annotated[Date | None, Field(alias="createdAt")] = None + eid: Annotated[String, Field(description="The `eid` is the object identifier.")] + id: Int + name: String | None = None + number: Int | None = None + publish_updated_at: Annotated[Date | None, Field(alias="publishUpdatedAt")] = None + published_at: Annotated[Date | None, Field(alias="publishedAt")] = None + published_description: Annotated[ + String | None, Field(alias="publishedDescription") + ] = None + published_number: Annotated[Int | None, Field(alias="publishedNumber")] = None + published_title: Annotated[String | None, Field(alias="publishedTitle")] = None + publishing_user: Annotated[User | None, Field(alias="publishingUser")] = None + slug: String | None = None + updated_at: Annotated[Date | None, Field(alias="updatedAt")] = None + updated_columns: Annotated[ + list[String | None] | None, Field(alias="updatedColumns") + ] = None + user: User | None = None + + +class ForgeVersionPage(GeneratedModel): + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + items: Annotated[ + list[ForgeVersion | None] | None, + Field(description="The actual records for the requested page"), + ] = None + page: Annotated[Int | None, Field(description="The requested page number")] = None + page_count: Annotated[ + Int | None, + Field(alias="pageCount", description="Total number of pages of results"), + ] = None + page_size: Annotated[ + Int | None, + Field(alias="pageSize", description="The requested number of rows per page"), + ] = None + row_count: Annotated[ + Int | None, + Field( + alias="rowCount", + description="Total number of rows found for the query before pagination. Capped when the query was given a `countLimit`.", + ), + ] = None + row_count_is_exact: Annotated[ + Boolean | None, + Field( + alias="rowCountIsExact", + description="False when `rowCount` stopped at the requested `countLimit`, meaning there are at least that many rows. True whenever the count ran to completion.", + ), + ] = None + + +class Organization(GeneratedModel): + """ + An entity that encapsulates a set of Workflows, templates, users, etc. + """ + + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + admin_users: Annotated[list[User | None] | None, Field(alias="adminUsers")] = None + available_signature_provider_types: Annotated[ + list[String | None] | None, Field(alias="availableSignatureProviderTypes") + ] = None + billing_email: Annotated[ + String | None, + Field( + alias="billingEmail", + description="The email used for all billing related correspondence.", + ), + ] = None + casts: Annotated[ + CastPage | None, + Field( + description="Returns a paginated Page that includes items (array of Casts), rowCount, pageCount, page, and pageSize." + ), + ] = None + child_organizations: Annotated[ + list[Organization | None] | None, Field(alias="childOrganizations") + ] = None + config: Any = None + created_at: Annotated[Date | None, Field(alias="createdAt")] = None + eid: Annotated[ + String | None, Field(description="The `eid` is the object identifier.") + ] = None + etch_complete_stats: Annotated[Any, Field(alias="etchCompleteStats")] = None + etch_packets: Annotated[EtchPacketPage | None, Field(alias="etchPackets")] = None + etch_templates: Annotated[ + list[EtchTemplate | None] | None, + Field( + alias="etchTemplates", + description="Etch templates in this organization, oldest first. Page through them with `limit` and `offset`.", + ), + ] = None + forge_maps: Annotated[list[ForgeMap | None] | None, Field(alias="forgeMaps")] = None + forges: list[Forge | None] | None = None + id: Int + is_personal: Annotated[Boolean | None, Field(alias="isPersonal")] = None + is_sso: Annotated[ + Boolean | None, + Field( + alias="isSso", + description="Is this Organization set up for SSO-based authentication or not.", + ), + ] = None + is_subscribed: Annotated[Boolean | None, Field(alias="isSubscribed")] = None + logo: Any = None + logo_url: Annotated[ + String | None, + Field( + alias="logoURL", + description="URL to your logo. You can update this on your organization settings page.", + ), + ] = None + name: String | None = None + organization_user_invites: Annotated[ + list[OrganizationUserInvite | None] | None, + Field(alias="organizationUserInvites"), + ] = None + organization_users: Annotated[ + list[OrganizationUser | None] | None, Field(alias="organizationUsers") + ] = None + parent_organization: Annotated[ + Organization | None, Field(alias="parentOrganization") + ] = None + payment_methods: Annotated[ + list[PaymentMethod | None] | None, Field(alias="paymentMethods") + ] = None + related_organizations: Annotated[ + list[Organization | None] | None, Field(alias="relatedOrganizations") + ] = None + remaining_users: Annotated[ + Int | None, + Field( + alias="remainingUsers", + description="Number of user slots remaining for this organization.", + ), + ] = None + resolved_config: Annotated[Any, Field(alias="resolvedConfig")] = None + signature_provider_type: Annotated[ + String | None, Field(alias="signatureProviderType") + ] = None + slug: String | None = None + stylesheet_url: Annotated[ + String | None, + Field( + alias="stylesheetURL", + description="Your organization's white-label CSS stylesheet. You can update this on your organization settings page.", + ), + ] = None + subscription_activity: Annotated[Any, Field(alias="subscriptionActivity")] = None + support_email: Annotated[ + String | None, + Field( + alias="supportEmail", + description="Your organization's support email address. Used as the reply-to address for emails sent to your users by Anvil.", + ), + ] = None + updated_at: Annotated[Date | None, Field(alias="updatedAt")] = None + use_test_signature_provider: Annotated[ + Boolean | None, Field(alias="useTestSignatureProvider") + ] = None + users: list[User | None] | None = None + webhook: Webhook | None = None + webhook_actions: Annotated[ + list[WebhookAction | None] | None, Field(alias="webhookActions") + ] = None + webhook_logs: Annotated[WebhookLogPage | None, Field(alias="webhookLogs")] = None + weld_complete_stats: Annotated[Any, Field(alias="weldCompleteStats")] = None + weld_complete_webhook_stats: Annotated[ + Any, Field(alias="weldCompleteWebhookStats") + ] = None + weld_groups: Annotated[list[WeldGroup | None] | None, Field(alias="weldGroups")] = ( + None + ) + welds: Annotated[ + WeldPage | None, + Field( + description="Returns a paginated Page that includes items (array of Welds), rowCount, pageCount, page, and pageSize." + ), + ] = None + + +class OrganizationPage(GeneratedModel): + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + items: Annotated[ + list[Organization | None] | None, + Field(description="The actual records for the requested page"), + ] = None + page: Annotated[Int | None, Field(description="The requested page number")] = None + page_count: Annotated[ + Int | None, + Field(alias="pageCount", description="Total number of pages of results"), + ] = None + page_size: Annotated[ + Int | None, + Field(alias="pageSize", description="The requested number of rows per page"), + ] = None + row_count: Annotated[ + Int | None, + Field( + alias="rowCount", + description="Total number of rows found for the query before pagination. Capped when the query was given a `countLimit`.", + ), + ] = None + row_count_is_exact: Annotated[ + Boolean | None, + Field( + alias="rowCountIsExact", + description="False when `rowCount` stopped at the requested `countLimit`, meaning there are at least that many rows. True whenever the count ran to completion.", + ), + ] = None + + +class OrganizationUser(GeneratedModel): + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + created_at: Annotated[Date | None, Field(alias="createdAt")] = None + external_id: Annotated[ + String | None, + Field( + alias="externalId", + description="A place to store your external system's unique identifier for this User.", + ), + ] = None + id: Int + organization: Organization | None = None + role: Annotated[ + String | None, + Field( + description="Role of this user within your org. Possible values: viewer, editor, billingAdmin, admin, tokenized, api" + ), + ] = None + updated_at: Annotated[Date | None, Field(alias="updatedAt")] = None + user: User | None = None + + +class OrganizationUserInvite(GeneratedModel): + """ + An invite sent for a user to join your organization. Once accepted, an + `OrganizationUser` will be created. + """ + + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + accepted_at: Annotated[Date | None, Field(alias="acceptedAt")] = None + created_at: Annotated[Date | None, Field(alias="createdAt")] = None + eid: Annotated[ + String | None, Field(description="The `eid` is the object identifier.") + ] = None + email: String | None = None + external_id: Annotated[ + String | None, + Field( + alias="externalId", + description="A place to store your external system's unique identifier for this User.", + ), + ] = None + id: Int + name: String | None = None + organization: Organization | None = None + revoked_at: Annotated[Date | None, Field(alias="revokedAt")] = None + role: Annotated[ + String | None, + Field( + description="Role of this user within your org. Possible values: viewer, editor, billingAdmin, admin, tokenized, api" + ), + ] = None + updated_at: Annotated[Date | None, Field(alias="updatedAt")] = None + + +class Signer(GeneratedModel): + """ + Represents a single signer. See `Signer.status` to indicate where a signer + is in the signing process. + """ + + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + alias_id: Annotated[ + String | None, + Field( + alias="aliasId", + description="The `Signer`'s user-specified id set on packet creation.", + ), + ] = None + allow_default_translations: Annotated[ + Boolean | None, Field(alias="allowDefaultTranslations") + ] = None + client_user_id: Annotated[ + String | None, + Field( + alias="clientUserId", + description="Your id for the person signing: a way to trace this signer back to a user in your system.", + ), + ] = None + completed_at: Annotated[ + Date | None, + Field( + alias="completedAt", + description="Timestamp when the `Signer` has finished signing.", + ), + ] = None + created_at: Annotated[Date | None, Field(alias="createdAt")] = None + document_group: Annotated[ + DocumentGroup | None, + Field( + alias="documentGroup", + description="A `DocumentGroup` references all signers and the documents after they have been filled and sent out for signature. See the `DocumentGroup`'s `status` for lifecycle information.", + ), + ] = None + eid: Annotated[ + String | None, Field(description="The `eid` is the object identifier.") + ] = None + email: String | None = None + finish_url: Annotated[ + String | None, + Field( + alias="finishURL", + description="The URL the user will be redirected to when they finish signing.", + ), + ] = None + id: Int + name: String | None = None + provider: Annotated[ + String | None, Field(description="Possible values: `etch` or `docusign`.") + ] = None + routing_order: Annotated[ + Int | None, + Field( + alias="routingOrder", + description="Numeric step in the signing process where this user signs. Signers are routed in sorted order.", + ), + ] = None + sign_action_type: Annotated[ + String | None, + Field( + alias="signActionType", + description="Indicates the method by which the user signs. Possible values: `email`, `embedded`, or `in-person`.", + ), + ] = None + signature_experience: Annotated[ + String | None, + Field( + alias="signatureExperience", + description="The inferred signing experience for this signer. Possible values: `signBeforeReviewing`, `signWhileReviewing`, `formCompletionOnly`, `acknowledgeOnly`.", + ), + ] = None + signer_tokens: Annotated[ + list[SignerToken | None] | None, + Field( + alias="signerTokens", + description="Get all `SignerTokens` this signer has available. These can tell you when a signer's token will be expiring, which token was used for signing, etc.", + ), + ] = None + status: Annotated[ + String | None, + Field( + description="Possible values: `sent`, `viewed`, `delivered`, `voided`, `autoresponded`, `faxpending`, `created`, `skipped`, `pending`, `ignored`, `declined`, `idCheckFailed`, `completed`" + ), + ] = None + submission: Annotated[ + Submission | None, + Field( + description="An associated `Submission`. This wil exist for signers who sign at the end of a Workflow." + ), + ] = None + support_email: Annotated[String | None, Field(alias="supportEmail")] = None + updated_at: Annotated[Date | None, Field(alias="updatedAt")] = None + user: Annotated[ + User | None, Field(description="A Anvil `User` associated with this signer.") + ] = None + + +class SignerPage(GeneratedModel): + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + items: Annotated[ + list[Signer | None] | None, + Field(description="The actual records for the requested page"), + ] = None + page: Annotated[Int | None, Field(description="The requested page number")] = None + page_count: Annotated[ + Int | None, + Field(alias="pageCount", description="Total number of pages of results"), + ] = None + page_size: Annotated[ + Int | None, + Field(alias="pageSize", description="The requested number of rows per page"), + ] = None + row_count: Annotated[ + Int | None, + Field( + alias="rowCount", + description="Total number of rows found for the query before pagination. Capped when the query was given a `countLimit`.", + ), + ] = None + row_count_is_exact: Annotated[ + Boolean | None, + Field( + alias="rowCountIsExact", + description="False when `rowCount` stopped at the requested `countLimit`, meaning there are at least that many rows. True whenever the count ran to completion.", + ), + ] = None + + +class SignerToken(GeneratedModel): + """ + Represents a token used to sign documents. See `Signer.signerTokens` to + fetch them. + + We do not expose the token itself, but you can use this object to get an + idea of what tokens the signer has available, whether they are valid, and + their expiration dates (`validUntil`). + """ + + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + created_at: Annotated[ + Date | None, + Field(alias="createdAt", description="Date when the token was created"), + ] = None + eid: Annotated[ + String | None, Field(description="The `eid` is the object identifier.") + ] = None + extra: Annotated[Any, Field(description="Extra data for the token.")] = None + has_signed: Annotated[ + Boolean | None, + Field( + alias="hasSigned", + description="`true` when this token was used to sign the `Signer`'s documents.", + ), + ] = None + id: Int + invalidated_at: Annotated[ + Date | None, + Field( + alias="invalidatedAt", + description="Date when the token was invalidated if explicitly invalidated. If invalid due to expiration, use `validUntil`.", + ), + ] = None + signed_at: Annotated[ + Date | None, + Field( + alias="signedAt", + description="Date when the token was used to sign documents.", + ), + ] = None + signer: Signer | None = None + type: Annotated[ + String | None, + Field( + description="Type of token issued. Tells you how the token was issued. Values: `api`, `sign-now`, `notify-signer`, `view`" + ), + ] = None + valid: Annotated[ + Boolean | None, + Field(description="`true` when this token is valid and can be used to sign."), + ] = None + valid_until: Annotated[ + Date | None, Field(alias="validUntil", description="Date of token expiration.") + ] = None + + +class Submission(GeneratedModel): + """ + An instance of a `Forge`. A `Submission` holds the data for one webform + submission on a particular `Forge`. + + See our [Workflow API guide](/docs/api/workflows) for more info. + """ + + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + completed_at: Annotated[Date | None, Field(alias="completedAt")] = None + completion_percentage: Annotated[ + Float | None, Field(alias="completionPercentage") + ] = None + continue_url: Annotated[ + String | None, + Field(alias="continueURL", description="URL to the next step in this webform."), + ] = None + created_at: Annotated[Date | None, Field(alias="createdAt")] = None + current_step: Annotated[Int | None, Field(alias="currentStep")] = None + eid: Annotated[ + String | None, Field(description="The `eid` is the object identifier.") + ] = None + forge: Forge | None = None + id: Int + is_excluded: Annotated[Boolean | None, Field(alias="isExcluded")] = None + payload: Annotated[Any, Field(description="The raw payload")] = None + payload_value: Annotated[ + Any, Field(alias="payloadValue", description="The raw payload without types") + ] = None + request_meta: Annotated[Any, Field(alias="requestMeta")] = None + resolved_payload: Annotated[ + Any, + Field( + alias="resolvedPayload", + description="The payload resolved with field aliases and field information. If you need to fetch the submission payload, use this.", + ), + ] = None + review_data: Annotated[Any, Field(alias="reviewData")] = None + signer: Signer | None = None + status: Annotated[ + String | None, + Field( + description="Possible values: `created`, `in-progress`, `waiting-to-sign`, `user-signed-and-waiting`, `users-turn-to-sign-ui`, `users-turn-to-sign-email`, `someone-else-signed-and-waiting`, `completed`" + ), + ] = None + total_steps: Annotated[Int | None, Field(alias="totalSteps")] = None + touched_by_user: Annotated[Boolean | None, Field(alias="touchedByUser")] = None + updated_at: Annotated[Date | None, Field(alias="updatedAt")] = None + user: User | None = None + weld_data: Annotated[WeldData | None, Field(alias="weldData")] = None + + +class SubmissionPage(GeneratedModel): + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + items: Annotated[ + list[Submission | None] | None, + Field(description="The actual records for the requested page"), + ] = None + page: Annotated[Int | None, Field(description="The requested page number")] = None + page_count: Annotated[ + Int | None, + Field(alias="pageCount", description="Total number of pages of results"), + ] = None + page_size: Annotated[ + Int | None, + Field(alias="pageSize", description="The requested number of rows per page"), + ] = None + row_count: Annotated[ + Int | None, + Field( + alias="rowCount", + description="Total number of rows found for the query before pagination. Capped when the query was given a `countLimit`.", + ), + ] = None + row_count_is_exact: Annotated[ + Boolean | None, + Field( + alias="rowCountIsExact", + description="False when `rowCount` stopped at the requested `countLimit`, meaning there are at least that many rows. True whenever the count ran to completion.", + ), + ] = None + + +class Tag(GeneratedModel): + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + config: Annotated[ + Any, + Field( + description='Config for the tag, e.g. { color: "red", description: "This is a red tag" }' + ), + ] = None + created_at: Annotated[Date | None, Field(alias="createdAt")] = None + eid: Annotated[ + String | None, Field(description="The `eid` is the object identifier.") + ] = None + id: Int + intended_for: Annotated[ + String | None, + Field( + alias="intendedFor", + description="The other tables this tag can be applied to, e.g. cast, weld_data, etch_packet, weld, etc.", + ), + ] = None + name: Annotated[String | None, Field(description="The name of the tag")] = None + organization: Organization | None = None + updated_at: Annotated[Date | None, Field(alias="updatedAt")] = None + + +class User(GeneratedModel): + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + created_at: Annotated[Date | None, Field(alias="createdAt")] = None + eid: Annotated[ + String | None, Field(description="The `eid` is the object identifier.") + ] = None + email: String | None = None + extra: Any = None + first_name: Annotated[String | None, Field(alias="firstName")] = None + last_name: Annotated[String | None, Field(alias="lastName")] = None + name: String | None = None + onboarding: Any = None + organization_users: Annotated[ + list[OrganizationUser | None] | None, Field(alias="organizationUsers") + ] = None + organizations: list[Organization | None] | None = None + preferences: UserPreferences + role: Annotated[ + String | None, + Field(description="What type of role the User has. Either `user` or `api`."), + ] = None + updated_at: Annotated[Date | None, Field(alias="updatedAt")] = None + verified_email: Annotated[Boolean | None, Field(alias="verifiedEmail")] = None + + +class UserPage(GeneratedModel): + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + items: Annotated[ + list[User | None] | None, + Field(description="The actual records for the requested page"), + ] = None + page: Annotated[Int | None, Field(description="The requested page number")] = None + page_count: Annotated[ + Int | None, + Field(alias="pageCount", description="Total number of pages of results"), + ] = None + page_size: Annotated[ + Int | None, + Field(alias="pageSize", description="The requested number of rows per page"), + ] = None + row_count: Annotated[ + Int | None, + Field( + alias="rowCount", + description="Total number of rows found for the query before pagination. Capped when the query was given a `countLimit`.", + ), + ] = None + row_count_is_exact: Annotated[ + Boolean | None, + Field( + alias="rowCountIsExact", + description="False when `rowCount` stopped at the requested `countLimit`, meaning there are at least that many rows. True whenever the count ran to completion.", + ), + ] = None + + +class UserUpload(GeneratedModel): + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + cast: Cast | None = None + created_at: Annotated[Date | None, Field(alias="createdAt")] = None + eid: Annotated[ + String | None, Field(description="The `eid` is the object identifier.") + ] = None + etch_packet: Annotated[EtchPacket | None, Field(alias="etchPacket")] = None + forge: Forge | None = None + id: Int + location: String | None = None + metadata: Any = None + updated_at: Annotated[Date | None, Field(alias="updatedAt")] = None + user: User | None = None + weld_data: Annotated[WeldData | None, Field(alias="weldData")] = None + + +class Webhook(GeneratedModel): + """ + A `Webhook` represents a webhook URL. Anvil will POST to webhook URLs on + specific actions. Both organization-wide and per-object webhook URLs are + represented by this object. See the [webhooks guide](/docs/api/webhooks) for + more information. + """ + + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + created_at: Annotated[Date | None, Field(alias="createdAt")] = None + creation_user: Annotated[User | None, Field(alias="creationUser")] = None + eid: Annotated[ + String | None, Field(description="The `eid` is the object identifier.") + ] = None + id: Int + inactive_at: Annotated[Date | None, Field(alias="inactiveAt")] = None + inactive_reason: Annotated[String | None, Field(alias="inactiveReason")] = None + organization: Organization | None = None + status: Annotated[ + String | None, Field(description="Prossible values: `active` or `inactive`.") + ] = None + token: Annotated[ + String | None, + Field(description="Use this token to make sure the request is from Anvil."), + ] = None + type: Annotated[ + String | None, + Field( + description="Webhook URLs that accept notifications for any object will be `prod`. Per-object webhook URLs will have type `adhoc`. Webhook URLs that accept notifications for a specific object will have type `webhookAction`." + ), + ] = None + updated_at: Annotated[Date | None, Field(alias="updatedAt")] = None + url: Annotated[String | None, Field(description="The URL Anvil will POST to.")] = ( + None + ) + webhook_actions: Annotated[ + list[WebhookAction | None] | None, Field(alias="webhookActions") + ] = None + + +class WebhookAction(GeneratedModel): + """ + A model that represents a subscription to actions on other objects in our system + that would trigger a [`Webhook`]({{Types.Webhook}}) call. + """ + + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + action: String + config: Any = None + created_at: Annotated[Date, Field(alias="createdAt")] + creation_user: Annotated[User, Field(alias="creationUser")] + eid: Annotated[String, Field(description="The `eid` is the object identifier.")] + object_eid: Annotated[String | None, Field(alias="objectEid")] = None + object_type: Annotated[String | None, Field(alias="objectType")] = None + updated_at: Annotated[Date, Field(alias="updatedAt")] + webhook: Webhook + + +class WebhookLog(GeneratedModel): + """ + A `WebhookLog` represents a single webhook call to your server. See the + [webhooks guide](/docs/api/webhooks) for more information. + """ + + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + action: Annotated[ + String | None, + Field(description="The webhook action type, e.g. `signerComplete`."), + ] = None + created_at: Annotated[Date | None, Field(alias="createdAt")] = None + eid: Annotated[ + String | None, Field(description="The `eid` is the object identifier.") + ] = None + id: Int + is_error: Annotated[ + Boolean | None, + Field( + alias="isError", + description="`true` when this call was considered an error. It will be an error when the status code is >= 400, or the request timed out.", + ), + ] = None + is_retry: Annotated[ + Boolean | None, + Field( + alias="isRetry", + description="`true` when this call was a retry of a previous call.", + ), + ] = None + milliseconds_to_finish: Annotated[ + Int | None, + Field( + alias="millisecondsToFinish", + description="Time it took to complete the webhook request.", + ), + ] = None + object_eid: Annotated[String | None, Field(alias="objectEid")] = None + object_metadata: Annotated[Any, Field(alias="objectMetadata")] = None + organization: Organization | None = None + original_webhook_log: Annotated[ + WebhookLog | None, + Field( + alias="originalWebhookLog", + description="If this call was a retry, `originalWebhookLog` will be `WebhookLog` that was retried.", + ), + ] = None + payload: Annotated[ + Any, Field(description="The decrypted webhook payload data.") + ] = None + root_object_type: Annotated[String | None, Field(alias="rootObjectType")] = None + root_object_url: Annotated[ + String | None, + Field( + alias="rootObjectURL", + description="Dashboard URL to the object that triggered the webhook event. e.g. it will link to a Workflow submission or an etch packet.", + ), + ] = None + status_code: Annotated[ + Int | None, + Field( + alias="statusCode", + description="The status code returned by your server for this call.", + ), + ] = None + + +class WebhookLogPage(GeneratedModel): + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + items: Annotated[ + list[WebhookLog | None] | None, + Field(description="The actual records for the requested page"), + ] = None + page: Annotated[Int | None, Field(description="The requested page number")] = None + page_count: Annotated[ + Int | None, + Field(alias="pageCount", description="Total number of pages of results"), + ] = None + page_size: Annotated[ + Int | None, + Field(alias="pageSize", description="The requested number of rows per page"), + ] = None + row_count: Annotated[ + Int | None, + Field( + alias="rowCount", + description="Total number of rows found for the query before pagination. Capped when the query was given a `countLimit`.", + ), + ] = None + row_count_is_exact: Annotated[ + Boolean | None, + Field( + alias="rowCountIsExact", + description="False when `rowCount` stopped at the requested `countLimit`, meaning there are at least that many rows. True whenever the count ran to completion.", + ), + ] = None + + +class Weld(GeneratedModel): + """ + A Weld is a Workflow. It holds the configuration that combines one or more + `Forge` objects with zero or more `Cast` objects to create a Workflow. + + See our [Workflow API guide](/docs/api/workflows) for more info. + """ + + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + allow_default_translations: Annotated[ + Boolean | None, Field(alias="allowDefaultTranslations") + ] = None + archived_at: Annotated[Date | None, Field(alias="archivedAt")] = None + available_signature_provider_types: Annotated[ + list[String | None] | None, Field(alias="availableSignatureProviderTypes") + ] = None + casts: list[Cast | None] | None = None + config: Annotated[ + Any, Field(description="Config object specifying files output, signers, etc.") + ] = None + created_at: Annotated[Date | None, Field(alias="createdAt")] = None + draft_step: Annotated[String | None, Field(alias="draftStep")] = None + eid: Annotated[ + String | None, + Field(description="The `eid` is the Weld/Workflow's object identifier."), + ] = None + expires_at: Annotated[Date | None, Field(alias="expiresAt")] = None + forges: list[Forge | None] | None = None + has_been_published: Annotated[Boolean | None, Field(alias="hasBeenPublished")] = ( + None + ) + has_signers: Annotated[Boolean | None, Field(alias="hasSigners")] = None + has_unpublished_changes: Annotated[ + Boolean | None, Field(alias="hasUnpublishedChanges") + ] = None + has_weld_datas: Annotated[Boolean | None, Field(alias="hasWeldDatas")] = None + id: Int + is_latest_version: Annotated[Boolean | None, Field(alias="isLatestVersion")] = None + is_published_version: Annotated[ + Boolean | None, Field(alias="isPublishedVersion") + ] = None + is_test: Annotated[Boolean | None, Field(alias="isTest")] = None + latest_draft_version_number: Annotated[ + Int | None, Field(alias="latestDraftVersionNumber") + ] = None + name: String | None = None + organization: Organization | None = None + published_at: Annotated[Date | None, Field(alias="publishedAt")] = None + published_number: Annotated[ + Int | None, + Field( + alias="publishedNumber", + description="Current published version number of this Weld", + ), + ] = None + published_slug: Annotated[ + String | None, + Field( + alias="publishedSlug", + description="The slug of the currently published version", + ), + ] = None + published_version_id: Annotated[Int | None, Field(alias="publishedVersionId")] = ( + None + ) + published_version_number: Annotated[ + Int | None, Field(alias="publishedVersionNumber") + ] = None + published_versions: Annotated[ + WeldVersionPage | None, Field(alias="publishedVersions") + ] = None + signature_provider_type: Annotated[ + String | None, Field(alias="signatureProviderType") + ] = None + slug: String | None = None + style_config: Annotated[Any, Field(alias="styleConfig")] = None + stylesheet_url: Annotated[String | None, Field(alias="stylesheetURL")] = None + submissions_count: Annotated[ + Int, + Field( + alias="submissionsCount", + description="Total number of submissions (WeldDatas) started by this Weld", + ), + ] + test_signer_complete_webhook_payload: Annotated[ + Any, Field(alias="testSignerCompleteWebhookPayload") + ] = None + test_weld_complete_webhook_payload: Annotated[ + Any, Field(alias="testWeldCompleteWebhookPayload") + ] = None + title: String | None = None + total_output_file_size: Annotated[ + Int | None, + Field( + alias="totalOutputFileSize", + description="Total size in bytes of all PDF casts that would be merged", + ), + ] = None + updated_at: Annotated[Date | None, Field(alias="updatedAt")] = None + version_id: Annotated[Int | None, Field(alias="versionId")] = None + version_number: Annotated[ + Int | None, + Field(alias="versionNumber", description="Current version number of this Weld"), + ] = None + versions: WeldVersionPage | None = None + visibility: String | None = None + weld_data_counts: Annotated[Any, Field(alias="weldDataCounts")] = None + weld_datas: Annotated[ + WeldDataPage | None, + Field( + alias="weldDatas", + description="Fetch all the `Weld`'s `WeldData`s (Workflow submissions)", + ), + ] = None + weld_groups: Annotated[list[WeldGroup | None] | None, Field(alias="weldGroups")] = ( + None + ) + + +class WeldData(GeneratedModel): + """ + An instance of a `Weld`. You can think of it as a "workflow submission". + A `WeldData` references `Submission`s for all the `Forge`s (webforms) + in the related `Weld` (workflow). + + See our [workflow API guide](/docs/api/workflows) for more info. + """ + + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + agents: Any = None + archived_at: Annotated[Date | None, Field(alias="archivedAt")] = None + completion_percentage: Annotated[ + Float | None, + Field( + alias="completionPercentage", + description="How complete is this workflow submission. Will be a value between 0 and 1.", + ), + ] = None + continue_url: Annotated[ + String | None, + Field( + alias="continueURL", + description="URL to the next form to be filled by the user. Use this after `WeldData` creation to get the URL to the form that should be filled first.", + ), + ] = None + created_at: Annotated[Date | None, Field(alias="createdAt")] = None + current_step_info: Annotated[Any, Field(alias="currentStepInfo")] = None + data_updated_at: Annotated[ + Date | None, + Field( + alias="dataUpdatedAt", + description="When a the data in any child `Submission` was last updated.", + ), + ] = None + display_title: Annotated[ + String | None, + Field(alias="displayTitle", description="The title displayed in the UI."), + ] = None + document_group: Annotated[ + DocumentGroup | None, + Field( + alias="documentGroup", + description="A `DocumentGroup` contains the documents after they have been filled and sent out for signature. The `DocumentGroup` will only exist here when that documents have been sent out for signature, or in the case of a workflow with no signers, when the workflow is completely finished. See the `DocumentGroup`'s `status` for lifecycle information.", + ), + ] = None + eid: Annotated[ + String | None, Field(description="The `eid` is the object identifier.") + ] = None + embedded_at: Annotated[Date | None, Field(alias="embeddedAt")] = None + etch_log_events: Annotated[ + list[EtchLog | None] | None, + Field( + alias="etchLogEvents", + description="An array of actions during the signing process when using Anvil's Etch as the signature provider. These objects detail actions when files are signed, downloaded, etc.", + ), + ] = None + excluded_files: Annotated[ + list[Any] | None, + Field( + alias="excludedFiles", + description="A file listing of all files excluded from the signing process.", + ), + ] = None + expires_at: Annotated[Date | None, Field(alias="expiresAt")] = None + files: Annotated[ + list[Any] | None, + Field( + description="A file listing of all files available in the `WeldData`. If there is a `DocumentGroup`, this will be equal to `DocumentGroup.files`." + ), + ] = None + forges_requiring_completion: Annotated[ + list[Forge | None] | None, Field(alias="forgesRequiringCompletion") + ] = None + has_pin: Annotated[Boolean | None, Field(alias="hasPin")] = None + has_signers: Annotated[Boolean | None, Field(alias="hasSigners")] = None + is_all_complete: Annotated[ + Boolean | None, + Field( + alias="isAllComplete", + description="`true` when status is `completed`: All forms have been filled, all signers have signed.", + ), + ] = None + is_complete: Annotated[ + Boolean | None, + Field( + alias="isComplete", + description="`true` when all forms in the workflow have been filled. There may be signers who need to sign.", + ), + ] = None + is_expired: Annotated[ + Boolean | None, + Field( + alias="isExpired", + description="This `WeldData` has expired, either by passing the `expiredAt` date, or by being locked.", + ), + ] = None + is_test: Annotated[ + Boolean | None, + Field( + alias="isTest", + description="`true` when it is a test workflow submission. You will not be charged for signatures on test submissions, and they will not count toward your monthly completed workflows.", + ), + ] = None + next_signer: Annotated[Signer | None, Field(alias="nextSigner")] = None + next_signers: Annotated[list[Signer | None] | None, Field(alias="nextSigners")] = ( + None + ) + number_remaining_signers: Annotated[ + Int | None, Field(alias="numberRemainingSigners") + ] = None + payload_can_be_updated: Annotated[ + Boolean | None, Field(alias="payloadCanBeUpdated") + ] = None + pin: String | None = None + status: Annotated[ + String | None, + Field( + description="Possible values: `created`, `in-progress`, `ready-to-sign`, `awaiting-signatures`, `completed`, `declined`" + ), + ] = None + stylesheet_url: Annotated[String | None, Field(alias="stylesheetURL")] = None + submissions: Annotated[ + list[Submission | None] | None, + Field( + description="An array of all the underlying `Submission` objects holding data from each webform." + ), + ] = None + updated_at: Annotated[ + Date | None, + Field( + alias="updatedAt", + description="When a column on the WeldData itself was updated.", + ), + ] = None + user_uploads: Annotated[ + list[UserUpload | None] | None, + Field( + alias="userUploads", + description="An array of files uploaded by users during the webform filling process.", + ), + ] = None + webhook_logs: Annotated[ + list[WebhookLog | None] | None, + Field( + alias="webhookLogs", + description="An array of webhook notification calls to your server related to this `WeldData`. Get the status codes, response times, and retry information from each `WebhookLog`.", + ), + ] = None + webhook_url: Annotated[ + String | None, + Field( + alias="webhookURL", + description="The specific webhook notification URL for this `WeldData`. We will POST to this URL when users take various actions.", + ), + ] = None + weld: Annotated[ + Weld | None, Field(description="The workflow this `WeldData` is related to.") + ] = None + weld_data_group: Annotated[WeldDataGroup | None, Field(alias="weldDataGroup")] = ( + None + ) + weld_data_log_events: Annotated[ + list[WeldDataLog | None] | None, + Field( + alias="weldDataLogEvents", + description="An array of actions taken during the form filling process.", + ), + ] = None + weld_version_id: Annotated[Int | None, Field(alias="weldVersionId")] = None + + +class WeldDataGroup(GeneratedModel): + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + archived_at: Annotated[Date | None, Field(alias="archivedAt")] = None + created_at: Annotated[Date | None, Field(alias="createdAt")] = None + data_updated_at: Annotated[Date | None, Field(alias="dataUpdatedAt")] = None + eid: Annotated[ + String | None, Field(description="The `eid` is the object identifier.") + ] = None + id: Int + is_test: Annotated[Boolean | None, Field(alias="isTest")] = None + title: String | None = None + updated_at: Annotated[Date | None, Field(alias="updatedAt")] = None + weld_datas: Annotated[list[WeldData | None] | None, Field(alias="weldDatas")] = None + weld_group: Annotated[WeldGroup | None, Field(alias="weldGroup")] = None + + +class WeldDataGroupPage(GeneratedModel): + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + items: Annotated[ + list[WeldDataGroup | None] | None, + Field(description="The actual records for the requested page"), + ] = None + page: Annotated[Int | None, Field(description="The requested page number")] = None + page_count: Annotated[ + Int | None, + Field(alias="pageCount", description="Total number of pages of results"), + ] = None + page_size: Annotated[ + Int | None, + Field(alias="pageSize", description="The requested number of rows per page"), + ] = None + row_count: Annotated[ + Int | None, + Field( + alias="rowCount", + description="Total number of rows found for the query before pagination. Capped when the query was given a `countLimit`.", + ), + ] = None + row_count_is_exact: Annotated[ + Boolean | None, + Field( + alias="rowCountIsExact", + description="False when `rowCount` stopped at the requested `countLimit`, meaning there are at least that many rows. True whenever the count ran to completion.", + ), + ] = None + + +class WeldDataLog(GeneratedModel): + """ + Represents a single action taken by a user while filling a webform within a Workflow. For + example, a `WeldDataLog` object will be created when a user completes a webform, downloads + a file, voids the packet associated packet, etc. + + All actions with a short description: + + ```md + accessed: Viewed + archived: Archived + completed: Completed Form + created: Created + download-csv-all: Downloaded + download-csv-individual-file: Downloaded + download-file-individual: Downloaded File + download-files-zip: Downloaded Zip + emailed-completed: Emailed + emailed: Emailed + locked: Locked + pin-code-changed: Changed PIN + pin-code-failed: Access Failed + pin-code-set: Set PIN + pin-code-unset: Unset PIN + pin-code-verified: Access Verified + saved-form: Saved form + signed: Signed + unarchived: Unarchived + unlocked: Unlocked + upload-file-accessed: Accessed file + upload-file: Uploaded + voided: Voided + ``` + """ + + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + action: Annotated[String | None, Field(description="The action taken.")] = None + created_at: Annotated[Date | None, Field(alias="createdAt")] = None + eid: Annotated[ + String | None, Field(description="The `eid` is the object identifier.") + ] = None + formatted_location: Annotated[ + String | None, + Field( + alias="formattedLocation", + description="Location of the user taking the action.", + ), + ] = None + id: Int + metadata: Annotated[Any, Field(description="Any action-specific information.")] = ( + None + ) + submission: Submission | None = None + user: Annotated[ + User | None, + Field(description="The Anvil `User` who took this action, if applicable."), + ] = None + weld_data: Annotated[WeldData | None, Field(alias="weldData")] = None + + +class WeldDataPage(GeneratedModel): + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + items: Annotated[ + list[WeldData | None] | None, + Field(description="The actual records for the requested page"), + ] = None + page: Annotated[Int | None, Field(description="The requested page number")] = None + page_count: Annotated[ + Int | None, + Field(alias="pageCount", description="Total number of pages of results"), + ] = None + page_size: Annotated[ + Int | None, + Field(alias="pageSize", description="The requested number of rows per page"), + ] = None + row_count: Annotated[ + Int | None, + Field( + alias="rowCount", + description="Total number of rows found for the query before pagination. Capped when the query was given a `countLimit`.", + ), + ] = None + row_count_is_exact: Annotated[ + Boolean | None, + Field( + alias="rowCountIsExact", + description="False when `rowCount` stopped at the requested `countLimit`, meaning there are at least that many rows. True whenever the count ran to completion.", + ), + ] = None + + +class WeldGroup(GeneratedModel): + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + created_at: Annotated[Date | None, Field(alias="createdAt")] = None + eid: Annotated[ + String | None, Field(description="The `eid` is the object identifier.") + ] = None + has_weld_data_groups: Annotated[ + Boolean | None, Field(alias="hasWeldDataGroups") + ] = None + id: Int + name: String | None = None + organization: Organization | None = None + slug: String | None = None + updated_at: Annotated[Date | None, Field(alias="updatedAt")] = None + weld_data_groups: Annotated[ + WeldDataGroupPage | None, Field(alias="weldDataGroups") + ] = None + welds: list[Weld | None] | None = None + + +class WeldPage(GeneratedModel): + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + items: Annotated[ + list[Weld | None] | None, + Field(description="The actual records for the requested page"), + ] = None + page: Annotated[Int | None, Field(description="The requested page number")] = None + page_count: Annotated[ + Int | None, + Field(alias="pageCount", description="Total number of pages of results"), + ] = None + page_size: Annotated[ + Int | None, + Field(alias="pageSize", description="The requested number of rows per page"), + ] = None + row_count: Annotated[ + Int | None, + Field( + alias="rowCount", + description="Total number of rows found for the query before pagination. Capped when the query was given a `countLimit`.", + ), + ] = None + row_count_is_exact: Annotated[ + Boolean | None, + Field( + alias="rowCountIsExact", + description="False when `rowCount` stopped at the requested `countLimit`, meaning there are at least that many rows. True whenever the count ran to completion.", + ), + ] = None + + +class WeldVersion(GeneratedModel): + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + config: Any = None + created_at: Annotated[Date | None, Field(alias="createdAt")] = None + eid: Annotated[String, Field(description="The `eid` is the object identifier.")] + id: Int + name: String | None = None + number: Int | None = None + publish_updated_at: Annotated[Date | None, Field(alias="publishUpdatedAt")] = None + published_at: Annotated[Date | None, Field(alias="publishedAt")] = None + published_description: Annotated[ + String | None, Field(alias="publishedDescription") + ] = None + published_number: Annotated[Int | None, Field(alias="publishedNumber")] = None + published_title: Annotated[String | None, Field(alias="publishedTitle")] = None + publishing_user: Annotated[User | None, Field(alias="publishingUser")] = None + slug: String | None = None + updated_at: Annotated[Date | None, Field(alias="updatedAt")] = None + updated_columns: Annotated[ + list[String | None] | None, Field(alias="updatedColumns") + ] = None + user: User | None = None + + +class WeldVersionPage(GeneratedModel): + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + items: Annotated[ + list[WeldVersion | None] | None, + Field(description="The actual records for the requested page"), + ] = None + page: Annotated[Int | None, Field(description="The requested page number")] = None + page_count: Annotated[ + Int | None, + Field(alias="pageCount", description="Total number of pages of results"), + ] = None + page_size: Annotated[ + Int | None, + Field(alias="pageSize", description="The requested number of rows per page"), + ] = None + row_count: Annotated[ + Int | None, + Field( + alias="rowCount", + description="Total number of rows found for the query before pagination. Capped when the query was given a `countLimit`.", + ), + ] = None + row_count_is_exact: Annotated[ + Boolean | None, + Field( + alias="rowCountIsExact", + description="False when `rowCount` stopped at the requested `countLimit`, meaning there are at least that many rows. True whenever the count ran to completion.", + ), + ] = None + + +Cast.model_rebuild() +CastTag.model_rebuild() +CastVersion.model_rebuild() +DocumentGroup.model_rebuild() +EtchLog.model_rebuild() +EtchPacket.model_rebuild() +EtchTemplate.model_rebuild() +Forge.model_rebuild() +ForgeVersion.model_rebuild() +Organization.model_rebuild() +OrganizationUser.model_rebuild() +Signer.model_rebuild() +Submission.model_rebuild() +UserUpload.model_rebuild() +Webhook.model_rebuild() +Weld.model_rebuild() +WeldData.model_rebuild() +WeldDataGroup.model_rebuild() diff --git a/python_anvil/types/operations.py b/python_anvil/types/operations.py new file mode 100644 index 00000000..a0ea7057 --- /dev/null +++ b/python_anvil/types/operations.py @@ -0,0 +1,1261 @@ +# Generated by dev/generate_types.py from schema/anvil_schema.graphql. Do not edit by hand. +# Run `make generate` to regenerate. +"""The arguments of every query and mutation in the Anvil GraphQL schema, as pydantic models. + +Field names are snake_case; `model_dump(by_alias=True, exclude_none=True)` gives the +camelCase variables to send. Input object and enum types come from `python_anvil.types.graphql`. +""" + +from __future__ import annotations + +from typing import Annotated, Any + +from pydantic import Field + +from python_anvil.types import graphql +from python_anvil.types._base import GeneratedModel + + +class CastArgs(GeneratedModel): + """Arguments of the `cast` query. + + Fetch a single `Cast`. A `Cast` is a PDF Template. See our + [terminology guide](/docs/api/getting-started#terminology) for more info. + """ + + eid: Annotated[ + str | None, + Field( + description="The Cast's `eid` (which is the same as the PDF template ID)" + ), + ] = None + version_number: Annotated[ + int | None, + Field( + alias="versionNumber", + description="Optional argument to request a specific version of the `Cast`. If not provided,\nthe last ***published*** version will be returned, unless it has not been published\nin which case the latest ***draft*** version will be returned.\n\nProviding `-1` is a special value that will return the latest\ndraft version of the `Cast`.", + ), + ] = None + + +class CurrentUserArgs(GeneratedModel): + """Arguments of the `currentUser` query. + + Fetches the currently logged in user. When used in an API context, this will + return your API user. The `currentUser` query is useful to fetch objects + in your organization. e.g. your `Organization`, its `Weld`s, etc. + """ + + pass + + +class EtchPacketArgs(GeneratedModel): + """Arguments of the `etchPacket` query. + + Fetch a single `EtchPacket`. An `EtchPacket` represents a signature + packet containing PDFs, signers, signed documents. This is the result of + `createEtchPacket`. See the + [e-signature API guide](/docs/api/e-signatures) for more info. + """ + + eid: str + + +class ForgeArgs(GeneratedModel): + """Arguments of the `forge` query. + + Fetch a single `Forge` by specifying _either_ (but not both) of the following argument sets: + 1. Just the `eid` for the Forge if you know it. + 2. Both the `organizationSlug` and `eidOrSlug`. + + A `Forge` holds configuration for a single webform. A `Forge` defines a + webform's fields, the page fields are on, their types, webform logic, etc. + Each Forge will be a member of only one `Weld` (Workflow). See our + [terminology guide](/docs/api/getting-started#terminology) for more info. + + A better way to fetch `Forge`s is via the `weld` query. + For example: + + ```graphql + weld (eid: weldEid) { + forges { + eid + slug + name + } + } + ``` + """ + + eid: Annotated[ + str | None, + Field( + description="If you are authenticated and know the EID, you can use this argument.\nBe sure to omit the `organizationSlug` and `eidOrSlug` arguments if passing `eid`." + ), + ] = None + eid_or_slug: Annotated[str | None, Field(alias="eidOrSlug")] = None + organization_slug: Annotated[str | None, Field(alias="organizationSlug")] = None + submission_eid: Annotated[ + str | None, + Field( + alias="submissionEid", + description="If you want to receive the Forge and its related, versioned items in the version associated with\na specific [`Submission`]({{Types.Submission}}) then pass that `Submission` EID here.", + ), + ] = None + version_number: Annotated[ + int | None, + Field( + alias="versionNumber", + description='Optional argument to request a specific version of the `Forge`. If not provided,\nthe last ***published*** version will be returned, unless it has not been published\nin which case the latest ***draft*** version will be returned.\n\nAll `Welds` and `Casts` that are related to this `Forge` will be returned\nin their state "as-of" the version requested for the `Forge`.\n\nProviding `-1` is a special value that will return the latest\ndraft version of the `Forge`, but also the latest draft version of any related\n`Weld` and `Casts`.', + ), + ] = None + weld_data_eid: Annotated[ + str | None, + Field( + alias="weldDataEid", + description="If you want to receive the Forge and its related, versioned items in the version associated with\na specific [`WeldData`]({{Types.WeldData}}) then pass that `WeldData` EID here.", + ), + ] = None + + +class IntegrationArgs(GeneratedModel): + """Arguments of the `integration` query. + + A connected (or connectable) third-party integration for the current + organization, e.g. "hubspot". Request only the fields you need — + `objectTypes` lazily calls out to the integration only when selected. + """ + + organization_eid: Annotated[str | None, Field(alias="organizationEid")] = None + type: str | None = None + + +class OrganizationArgs(GeneratedModel): + """Arguments of the `organization` query. + + Fetch a single `Organization`. You obviously will only be able to access + organizations you are a part of. + + A better way to fetch this information is via the `currentUser` query. + For example: + + ```graphql + currentUser { + organizations { + eid + slug + name + } + } + ``` + """ + + organization_eid: Annotated[str | None, Field(alias="organizationEid")] = None + organization_slug: Annotated[str | None, Field(alias="organizationSlug")] = None + + +class SignerArgs(GeneratedModel): + """Arguments of the `signer` query. + + Fetches a single `Signer`. + + An alternate way to fetch signers is via a `DocumentGroup` object. For + example, use the `etchPacket` or `weldData` query, then dig down to the + signers in your query. + """ + + eid: str + + +class SubmissionArgs(GeneratedModel): + """Arguments of the `submission` query. + + Fetches a single `Submission`. A `Submission` holds data submitted to a + webform. See our + [terminology guide](/docs/api/getting-started#terminology) for more info. + + A better way to fetch submissions is via the `weldData` query. For example: + + ```graphql + weldData (eid: weldDataEid) { + displayTitle + submissions { + eid + resolvedPayload + } + } + ``` + """ + + eid: str + force_create: Annotated[bool | None, Field(alias="forceCreate")] = None + forge_eid_or_slug: Annotated[str, Field(alias="forgeEidOrSlug")] + organization_slug: Annotated[str, Field(alias="organizationSlug")] + timezone: str | None = None + + +class WeldArgs(GeneratedModel): + """Arguments of the `weld` query. + + Fetch a single `Weld`. This query is a good way to fetch all of a + Workflow's submissions (`WeldData`s). See the `Weld`'s `weldDatas` + resolver. + + Specify either the `Weld`'s `eid` _or_ both the `Weld`'s `slug` and + the `Weld`'s `organizationSlug`. + + See the [Workflow API guide](/docs/api/workflows) for more information on + query usage. + """ + + eid: Annotated[str | None, Field(description="The Workflow / Weld's `eid`")] = None + organization_slug: Annotated[str | None, Field(alias="organizationSlug")] = None + slug: str | None = None + version_number: Annotated[ + int | None, + Field( + alias="versionNumber", + description='Optional argument to request a specific version of the `Weld`. If not provided,\nthe last ***published*** version will be returned, unless it has not been published\nin which case the latest ***draft*** version will be returned.\n\nAll `Forges` and `Casts` that are related to this `Weld` will be returned\nin their state "as-of" the version requested for the `Weld`.\n\nProviding `-1` is a special value that will return the latest\ndraft version of the `Weld`, but also the latest draft version of any related\n`Forges` and `Casts`.', + ), + ] = None + + +class WeldDataArgs(GeneratedModel): + """Arguments of the `weldData` query. + + Fetch a single `WeldData` (Workflow submission). If you need to fetch all + `WeldData`s for a given `Weld`, see the `weld` query. + + See the [Workflow API guide](/docs/api/workflows) for more information on + query usage. + """ + + eid: str + + +class CreateCastArgs(GeneratedModel): + """Arguments of the `createCast` mutation. + + Create a `Cast` with the specified properties. This will require you to publish the `Cast` + via the [`publishCast`]({{Mutations.publishCast}}) mutation before it will go live. + """ + + advanced_detect_fields: Annotated[ + bool | None, + Field( + alias="advancedDetectFields", + description="Uses AI to help analyze the document's fields. Will implicitly set `detectFields` to `true`.", + ), + ] = None + allowed_alias_ids: Annotated[ + list[str | None] | None, Field(alias="allowedAliasIds") + ] = None + detect_boxes_advanced: Annotated[ + bool | None, + Field( + alias="detectBoxesAdvanced", + description="Uses AI to help detect the document's fields. Will implicitly set `detectFields` to `true`.", + ), + ] = None + detect_fields: Annotated[bool | None, Field(alias="detectFields")] = None + file: Any + is_template: Annotated[bool | None, Field(alias="isTemplate")] = None + organization_eid: Annotated[str | None, Field(alias="organizationEid")] = None + schema_: Annotated[ + Any | None, + Field( + alias="schema", + description='The schema input, interpreted according to `schemaSource`:\n\n- `schemaSource: "json"` -- `schema` *is* your aliasIds, passed\n inline. This is the canonical way to supply your own custom field\n aliases. Accepts either the object form or the array shorthand. For\n example:\n ```\n schemaSource: "json"\n schema: { employeeName: { description: "The employee\'s full name" } }\n ```\n or\n ```\n schemaSource: "json"\n schema: ["employeeName", "employeeId"]\n ```\n\n- If `schemaSource` is an integration (currently only `"hubspot"`),\n `schema` tells us which object type(s) on that integration to build\n `aliasIds` from. We look up each object type\'s properties from the\n integration and normalize them into the `aliasIds` shape for you.\n How each aliasId is named depends on how many object types you pass:\n - One object type: the bare property name. For example,\n `schema: { objectTypes: ["contacts"] }` gives aliasIds like\n `firstname` and `email`.\n - More than one: `${objectType}.${propertyName}`, so properties two\n object types share stay separate. For example,\n `schema: { objectTypes: ["contacts", "companies"] }` gives\n aliasIds like `contacts.email`, `contacts.phone`, and\n `companies.phone`.\n\n Detected fields that match a property are tagged with its aliasId,\n so these are also the keys to use in `data` when filling the Cast.\n\nRequires `advancedDetectFields: true`.', + ), + ] = None + schema_source: Annotated[ + str | None, + Field( + alias="schemaSource", + description='The source of the input schema (e.g. "json", "hubspot") used to create this cast. Pair it with `schema` (see below).', + ), + ] = None + title: str | None = None + + +class CreateEtchPacketArgs(GeneratedModel): + """Arguments of the `createEtchPacket` mutation. + + Create an e-signature packet. See the + [Etch e-sign guide](/docs/api/e-signatures) for usage information. + """ + + advanced_create: Annotated[ + bool | None, + Field( + alias="advancedCreate", + description="Uses AI to label uploaded documents, discover signers, and connect signers to fields.", + ), + ] = None + allow_updates: Annotated[ + bool | None, + Field( + alias="allowUpdates", + description="When enabled, the EtchPacket payload can be updated. Requires a plan that allows EtchPacket updates.", + ), + ] = None + create_cast_templates_from_uploads: Annotated[ + bool | None, + Field( + alias="createCastTemplatesFromUploads", + description="Set to `true` to save uploaded PDFs to your PDF templates.", + ), + ] = None + data: Annotated[ + Any | None, + Field( + description="Specifies data to fill documents in the `files` array. See the guide for details." + ), + ] = None + detect_boxes_advanced: Annotated[ + bool | None, + Field( + alias="detectBoxesAdvanced", + description="Uses AI to help detect fields on uploaded document. Will implicitly set `detectFields` to `true`.", + ), + ] = None + duplicate_casts: Annotated[ + bool | None, + Field( + alias="duplicateCasts", + description="Set to `true` to duplicate all referenced PDF templates on packet\ncreation. Setting to true is useful if you would like to edit the PDFs\nin the UI after packet creation.", + ), + ] = None + enable_emails: Annotated[ + Any | None, + Field( + alias="enableEmails", + description="Enable all, none, or specific emails for this packet. When enabled, an\nemail type will adhere to the organization settings for that email type.\nFor example, when `signerViewAdmin` is enabled, an email will only be\nsent to organization members specified in the organization settings.\n\nPossible values: `true`, `false`, or an array of strings with any of\nthe following `'signerViewAdmin'`, `'signerCompleteAdmin'`, or\n`'etchCompleteAdmin'`.\n\nThe default is `false`.", + ), + ] = None + exclude_certificate_from_signer_downloads: Annotated[ + bool | None, + Field( + alias="excludeCertificateFromSignerDownloads", + description="When enabled, the signature certificate is withheld from what signers\ndownload (finish page, per-file download, and the combined zip). It is\nstill generated, stored, and available to your organization's users.", + ), + ] = None + files: Annotated[ + list[graphql.EtchFile] | None, + Field( + description="Specifies files that will be in the resulting packet. See the guide for details." + ), + ] = None + finish_page_options: Annotated[ + Any | None, + Field( + alias="finishPageOptions", + description="Allows customization and whitelabeling of the signer finish page. See the guide for details.", + ), + ] = None + is_draft: Annotated[ + bool | None, + Field( + alias="isDraft", + description="When set to `false`, the packet will immediately be sent to the first\nsigner. When set to `true`, the resulting `EtchPacket` will not be\nimmediately sent. You will need to send the packet via the UI or\nthe [`sendEtchPacket`](#operation-sendetchpacket-Mutations) mutation.", + ), + ] = None + is_test: Annotated[ + bool | None, + Field( + alias="isTest", + description="When `true`, the resulting packet will be in test mode. You will not\nbe charged for test packets. Test packets will not use valid signatures.\nTest packets only email verified members of your organization; sending\none to any other email signer is refused.", + ), + ] = None + merge_pdfs: Annotated[ + bool | None, + Field( + alias="mergePDFs", + description="When enabled, merges all PDFs before users sign. All signers will sign\none (potentially large) PDF.", + ), + ] = None + name: str | None = None + organization_eid: Annotated[str | None, Field(alias="organizationEid")] = None + reply_to_email: Annotated[ + str | None, + Field( + alias="replyToEmail", + description="Overrides the replyTo header on emails sent to signers.\nBy default, this will be the support email listed in your organization settings.", + ), + ] = None + reply_to_name: Annotated[ + str | None, + Field( + alias="replyToName", + description="Overrides the replyTo header on emails sent to signers.\nBy default, this will be your organization name.", + ), + ] = None + require_signatures: Annotated[ + bool | None, + Field( + alias="requireSignatures", + description="When `true`, every signer on the packet must have a signature or\nsignature initial field assigned; sending a packet with a signer missing\none throws an error. When omitted, falls back to the organization's\n`requireSignatures` config, then defaults to `false`. Pass `false`\nto explicitly override an organization that has the setting enabled.", + ), + ] = None + signature_email_body: Annotated[ + str | None, + Field( + alias="signatureEmailBody", + description="Customizes the email body on signer emails", + ), + ] = None + signature_email_subject: Annotated[ + str | None, + Field( + alias="signatureEmailSubject", + description="Customizes the email subject on signer emails", + ), + ] = None + signature_page_options: Annotated[ + Any | None, + Field( + alias="signaturePageOptions", + description="Allows customization and whitelabeling of the signature page. See the guide for details.", + ), + ] = None + signature_provider: Annotated[str | None, Field(alias="signatureProvider")] = None + signature_recipients: Annotated[ + list[Any] | None, + Field( + alias="signatureRecipients", + description="BCC recipients for DocuSign envelopes. Only relevant when `signatureProvider` is set to `'docusign'`.\n\nEach recipient will receive a copy of the completed signature packet via email.\n\nRequired fields:\n- `name`: Recipient name\n- `email`: Recipient email address\n- `type`: Must be `'carbonCopy'`\n\nOptional fields:\n- `disableInTestMode`: When `true`, removes recipient when packet is in test mode\n- `roleName`: Role name for the recipient\n- `accessCode`: Access code for the recipient\n- `templateAccessCodeRequired`: Whether access code is required", + ), + ] = None + signers: Annotated[ + list[Any] | None, + Field( + description="Specifies signers who will sign documents in the `files` array. See the guide for details.\n\nWhen `signatureProvider` is set to `'docusign'`, additional DocuSign-specific fields are available:\n- `requireIdCheck`: Boolean or string ('kb', 'sms', 'smskb', 'idsms') to require ID verification\n- `requireIdCheckInTest`: Boolean to require ID verification even in test mode\n- `idCheckPhone`: Phone number for ID verification (required when requireIdCheck is 'sms', 'smskb', or 'idsms')\n- `accessCode`: Access code for the signer" + ), + ] = None + webhook_url: Annotated[ + str | None, + Field( + alias="webhookURL", + description="Allows you to set a webhook URL called specifically for actions on the resulting `EtchPacket`", + ), + ] = None + + +class CreateForgeArgs(GeneratedModel): + """Arguments of the `createForge` mutation. + + Create a `Forge` with the specified properties. This will require you to publish the `Weld` + via the [`publishWeld`]({{Mutations.publishWeld}}) mutation before it will go live with the Workflow. + """ + + cast_eid: Annotated[str | None, Field(alias="castEid")] = None + cast_field_ids: Annotated[Any | None, Field(alias="castFieldIds")] = None + config: Any | None = None + name: str + slug: str + weld_eid: Annotated[str, Field(alias="weldEid")] + + +class CreateSubmissionArgs(GeneratedModel): + """Arguments of the `createSubmission` mutation.""" + + forge_eid: Annotated[str, Field(alias="forgeEid")] + weld_data_eid: Annotated[str, Field(alias="weldDataEid")] + + +class CreateWebhookArgs(GeneratedModel): + """Arguments of the `createWebhook` mutation.""" + + organization_eid: Annotated[str | None, Field(alias="organizationEid")] = None + organization_slug: Annotated[str | None, Field(alias="organizationSlug")] = None + url: str | None = None + + +class CreateWebhookActionArgs(GeneratedModel): + """Arguments of the `createWebhookAction` mutation. + + Create a Webhook Action to subscribe to events on objects. + + Here is a list of all the actions and the object types that are supported by them: + - [`documentGroupCreate`](/docs/api/webhooks/#documentgroupcreate) => [`DocumentGroup`]({{Types.DocumentGroup}}) + - [`etchPacketComplete`](/docs/api/webhooks/#etchpacketcomplete) => [`EtchPacket`]({{Types.EtchPacket}}) + - [`forgeComplete`](/docs/api/webhooks/#forgecomplete) => [`Forge`]({{Types.Forge}}), [`Submission`]({{Types.Submission}}), [`WeldData`]({{Types.WeldData}}), [`Weld`]({{Types.Weld}}) + - [`signerUpdateStatus`](/docs/api/webhooks/#signerupdatestatus) => [`Signer`]({{Types.Signer}}), [`EtchPacket`]({{Types.EtchPacket}}), [`WeldData`]({{Types.WeldData}}), [`Weld`]({{Types.Weld}}) + - [`signerComplete`](/docs/api/webhooks/#signercomplete) => [`Signer`]({{Types.Signer}}), [`EtchPacket`]({{Types.EtchPacket}}), [`WeldData`]({{Types.WeldData}}), [`Weld`]({{Types.Weld}}) + - [`weldCreate`](/docs/api/webhooks/#weldcreate) => [`Organization`]({{Types.Organization}}) + - [`weldComplete`](/docs/api/webhooks/#weldcomplete) => [`WeldData`]({{Types.WeldData}}), [`Weld`]({{Types.Weld}}) + + See the [Webhook Action documentation](/docs/api/webhooks/#webhook-actions) for usage information. + """ + + action: Annotated[ + str, + Field( + description="The action you'd like to subscribe to on the object(s).\nYou can also use `*` to monitor all actions." + ), + ] + config: graphql.WebhookActionConfigInput | None = None + object_eid: Annotated[ + str, + Field( + alias="objectEid", + description="The `eid` of the specific object you'd like to monitor.\nYou can also use `*` to monitor all objects of the specified type(s).", + ), + ] + object_type: Annotated[ + str, + Field( + alias="objectType", + description="The type of object that you'd like to monitor for events.\nYou can also use `*` to monitor all object types.", + ), + ] + organization_eid: Annotated[ + str | None, + Field( + alias="organizationEid", + description="If you want to create a new [`Webhook`]({{Types.Webhook}}) on-the-fly, you must specify the Organization `eid` here.", + ), + ] = None + url: Annotated[ + str | None, + Field( + description="If you want to create a new [`Webhook`]({{Types.Webhook}}) on-the-fly, you must specify the URL here." + ), + ] = None + webhook_eid: Annotated[ + str | None, + Field( + alias="webhookEid", + description="The `eid` of a pre-existing [`Webhook`]({{Types.Webhook}}) that you'd like to use for this Webhook Action.\nA new `Webhook` can be created as well.", + ), + ] = None + + +class CreateWeldArgs(GeneratedModel): + """Arguments of the `createWeld` mutation. + + Create a `Weld` with the specified properties. This will require you to publish the `Weld` + via the [`publishWeld`]({{Mutations.publishWeld}}) mutation before it will go live. + """ + + advanced_create: Annotated[ + bool | None, + Field( + alias="advancedCreate", + description="Uses AI to generate webform pages, create signers, and connect webform fields to supplied documents.", + ), + ] = None + advanced_detect_fields: Annotated[ + bool | None, + Field( + alias="advancedDetectFields", + description="Uses AI to help detect and analyze uploaded documents' fields.", + ), + ] = None + cast_eid: Annotated[str | None, Field(alias="castEid")] = None + config: Any | None = None + create_cast_templates_from_uploads: Annotated[ + bool | None, + Field( + alias="createCastTemplatesFromUploads", + description="Set to `true` to save uploaded PDFs to your PDF templates.", + ), + ] = None + detect_boxes_advanced: Annotated[ + bool | None, + Field( + alias="detectBoxesAdvanced", + description="Uses AI to help detect fields on uploaded documents.", + ), + ] = None + draft_step: Annotated[str | None, Field(alias="draftStep")] = None + files: list[graphql.WeldFile | None] | None = None + name: str | None = None + organization_eid: Annotated[str | None, Field(alias="organizationEid")] = None + slug: str | None = None + visibility: str | None = None + + +class CreateWeldDataArgs(GeneratedModel): + """Arguments of the `createWeldData` mutation.""" + + is_test: Annotated[bool | None, Field(alias="isTest")] = None + webhook_url: Annotated[str | None, Field(alias="webhookURL")] = None + weld_data_group_eid: Annotated[str | None, Field(alias="weldDataGroupEid")] = None + weld_eid: Annotated[str, Field(alias="weldEid")] + + +class DestroySubmissionArgs(GeneratedModel): + """Arguments of the `destroySubmission` mutation.""" + + eid: str + + +class DisconnectDocusignArgs(GeneratedModel): + """Arguments of the `disconnectDocusign` mutation.""" + + organization_slug: Annotated[str, Field(alias="organizationSlug")] + + +class DuplicateCastArgs(GeneratedModel): + """Arguments of the `duplicateCast` mutation.""" + + eid: str + name: str | None = None + organization_eid: Annotated[str | None, Field(alias="organizationEid")] = None + title: str | None = None + + +class DuplicateWeldArgs(GeneratedModel): + """Arguments of the `duplicateWeld` mutation.""" + + eid: str + name: str | None = None + organization_eid: Annotated[str, Field(alias="organizationEid")] + slug: str | None = None + visibility: str | None = None + + +class ExpireSessionTokenArgs(GeneratedModel): + """Arguments of the `expireSessionToken` mutation. + + Expires the `UserToken` associated with the provided `requestTokenEid`. Any authentication attempts + made using that token will be rejected. + """ + + request_token_eid: Annotated[ + str, + Field( + alias="requestTokenEid", + description="The EID of the request token that was returned from the [`generateEmbedURL`]({{Mutations.generateEmbedURL}}) mutation.\nUsed to determine which session token to invalidate.", + ), + ] + + +class ExpireSignerTokensArgs(GeneratedModel): + """Arguments of the `expireSignerTokens` mutation. + + Expires all valid `SignerTokens` for a specific `Signer`. Signers who + visit links with expired tokens will be shown a "Token Expired" error, or + will be [redirected to your + service](/docs/api/e-signatures#handling-signing-errors) if there is a + `redirectURL` set on the signer. + + If the `Signer` has already completed signing, this mutation will throw an + error. If you would like the `Signer` to be able to sign again, email + signers will need to be notified via `sendEtchPacket`, and embedded + signers will need a new signing link generated by `generateEtchSignURL`. + """ + + signer_eid: Annotated[str, Field(alias="signerEid")] + + +class ForgeSubmitArgs(GeneratedModel): + """Arguments of the `forgeSubmit` mutation. + + Submits data to a Workflow's webform (a `Forge`). You can use this + mutation to start a Workflow with data from your system and submit data to + an _existing_ `Submission`. + + See the [Workflow API guide](/docs/api/workflows) for more information on + usage scenarios. + """ + + complete: bool | None = None + current_step: Annotated[int | None, Field(alias="currentStep")] = None + enforce_payload_valid_on_create: Annotated[ + bool | None, Field(alias="enforcePayloadValidOnCreate") + ] = None + forge_eid: Annotated[str, Field(alias="forgeEid")] + group_array_id: Annotated[str | None, Field(alias="groupArrayId")] = None + group_array_index: Annotated[int | None, Field(alias="groupArrayIndex")] = None + is_test: Annotated[bool | None, Field(alias="isTest")] = None + payload: Annotated[ + Any, + Field( + description="A `{ key: value }` object where `key` is the id or field alias of the\nwebform (`Forge`) field, and value is the data you'd like to send.\n\nData is additive: you only need to submit data you wish to update." + ), + ] + submission_eid: Annotated[str | None, Field(alias="submissionEid")] = None + timezone: str | None = None + webhook_url: Annotated[str | None, Field(alias="webhookURL")] = None + weld_data_eid: Annotated[str | None, Field(alias="weldDataEid")] = None + + +class GenerateEmbedURLArgs(GeneratedModel): + """Arguments of the `generateEmbedURL` mutation. + + Generate a URL for embedding the PDF template editor, the Etch packet + builder, or the Workflow builder in your app. Embedding editors is an + enterprise feature. Please contact sales@useanvil.com if you are interested + in editor embedding. + """ + + eid: Annotated[ + str, + Field(description="The EID of the object you want to give embedded access to."), + ] + metadata: Annotated[ + Any | None, + Field( + description="Any information that you would like to associate with this embedded URL request" + ), + ] = None + options: Annotated[ + Any | None, + Field( + description="Options to control many parts of a token user's experience.\nCurrently only supported when `type` is `edit-pdf-template`." + ), + ] = None + require_signatures: Annotated[ + bool | None, + Field( + alias="requireSignatures", + description="For `type: edit-etch-packet` embeds only. When `true` (default),\nevery signer must have at least one signature or initial field;\nsigner-experience inference is not surfaced to the packet builder.\nWhen `false`, signers without signature fields are allowed (the\npacket builder UI shows a pre-send review of each signer's\ninferred experience). Has no effect for other embed types.", + ), + ] = None + type: Annotated[ + str, + Field( + description="The type of object you want to create an embedded URL for. Allowed values are `edit-etch-packet`, `edit-pdf-template`, `edit-weld`, `submit-webform`, `edit-workflow`" + ), + ] + valid_for_seconds: Annotated[ + int | None, + Field( + alias="validForSeconds", + description="The number of seconds from now that the embedded access will be valid for.\n\nMust provide either `validUntil` or `validForSeconds` arguments.", + ), + ] = None + valid_until: Annotated[ + str | None, + Field( + alias="validUntil", + description="An ISO compliant date-time string such as `2030-01-01T00:00:00+00:00` that indicates when the embedded access will be valid until.\n\n Must provide either `validUntil` or `validForSeconds` arguments.", + ), + ] = None + + +class GenerateEtchSignURLArgs(GeneratedModel): + """Arguments of the `generateEtchSignURL` mutation. + + Generate a signing URL for embedded signers. See the + [e-sign API guide on embedding signers](/docs/api/e-signatures#controlling-the-signature-process-with-embedded-signers) + for more info. + """ + + client_user_id: Annotated[ + str, + Field( + alias="clientUserId", + description="Your id for the person signing: a way to trace this signer back to a user in your system.", + ), + ] + signer_eid: Annotated[str, Field(alias="signerEid")] + + +class MergeWeldsArgs(GeneratedModel): + """Arguments of the `mergeWelds` mutation.""" + + auto_publish: Annotated[bool | None, Field(alias="autoPublish")] = None + auto_publish_description: Annotated[ + str | None, Field(alias="autoPublishDescription") + ] = None + auto_publish_title: Annotated[str | None, Field(alias="autoPublishTitle")] = None + deduplicate_webform_fields: Annotated[bool, Field(alias="deduplicateWebformFields")] + name: str + organization_eid: Annotated[str, Field(alias="organizationEid")] + slug: str + weld_eids: Annotated[list[str] | None, Field(alias="weldEids")] = None + + +class NotifySignerArgs(GeneratedModel): + """Arguments of the `notifySigner` mutation. + + Notify a signer that it is their turn to sign. You can think of this as a + "resend email" action for signers. This mutation will email signers with + `signerType: 'email'` asking them to sign their portion of the signature + packet. It will have no effect on signers with + `signerType: 'embedded'` signers, it will error if the signer has already + signed, or if it is not yet the signer's turn to sign. + """ + + signer_eid: Annotated[str, Field(alias="signerEid")] + + +class PublishCastArgs(GeneratedModel): + """Arguments of the `publishCast` mutation. + + Publish a `Cast` by updating the `Cast` with all un-published changes. + + This is required to have any changes from an [`updateCast`]({{Mutations.updateCast}}) take effect. + """ + + description: str | None = None + eid: Annotated[str, Field(description="The object's `eid`")] + title: Annotated[ + str, + Field( + description="A title for this version. This will be displayed in the version history." + ), + ] + + +class PublishWeldArgs(GeneratedModel): + """Arguments of the `publishWeld` mutation. + + Publish a `Weld` by updating the `Weld` with all un-published changes. + + This is required to have any changes from an [`updateWeld`]({{Mutations.updateWeld}}) take effect. + """ + + description: str | None = None + eid: Annotated[str, Field(description="The object's `eid`")] + migrate_open_weld_datas: Annotated[ + bool | None, + Field( + alias="migrateOpenWeldDatas", + description="A boolean indicating whether you'd like to migrate all open workflow submissions to the\nnew published version, or leave them pegged to the version under which they were created.", + ), + ] = None + title: Annotated[ + str, + Field( + description="A title for this version. This will be displayed in the version history." + ), + ] + + +class RemoveEtchPacketArgs(GeneratedModel): + """Arguments of the `removeEtchPacket` mutation.""" + + eid: str + + +class RemoveWebhookArgs(GeneratedModel): + """Arguments of the `removeWebhook` mutation.""" + + eid: str + + +class RemoveWebhookActionArgs(GeneratedModel): + """Arguments of the `removeWebhookAction` mutation. + + Remove a Webhook Action and stop receiving Webhook calls for it. + """ + + eid: Annotated[ + str, Field(description="The EID of the Webhook Action you want to remove.") + ] + + +class RemoveWeldDataArgs(GeneratedModel): + """Arguments of the `removeWeldData` mutation.""" + + eid: str + + +class RetryWebhookLogArgs(GeneratedModel): + """Arguments of the `retryWebhookLog` mutation.""" + + eid: str + + +class SendEtchPacketArgs(GeneratedModel): + """Arguments of the `sendEtchPacket` mutation. + + This will move a `draft` `EtchPacket` into sent mode (`status` == + `sent`). If the first signer is an email signer, this will kick off an + email, with a link to sign their portion of the packet. + + You can call this mutation after a packet is sent to "resend" signature + emails to your email signers. For example, you can send a new email via + `sendEtchPacket` if they lose the email, or if they have an email with an + expired token. This mutation will always email the first signer who has not + yet signed. + + When your signers are set up as `embedded` signers, they will _not_ be + emailed. You must generate signing URLs via the + [`generateEtchSignURL`]({{Mutations.generateEtchSignURL}}) mutation. See + [our e-signature guide on embedded signers](/docs/api/e-signatures#controlling-the-signature-process-with-embedded-signers) + for more info. + """ + + eid: str + + +class SkipSignerArgs(GeneratedModel): + """Arguments of the `skipSigner` mutation.""" + + clear_name_and_email: Annotated[bool | None, Field(alias="clearNameAndEmail")] = ( + None + ) + signer_eid: Annotated[str, Field(alias="signerEid")] + + +class StartIntegrationConnectSessionArgs(GeneratedModel): + """Arguments of the `startIntegrationConnectSession` mutation. + + Mint a short-lived Nango Connect session token for the given integration + (e.g. "hubspot"), scoped to the current organization. The frontend uses + this token to open Nango's hosted Connect UI popup. + """ + + integration: str | None = None + organization_eid: Annotated[str | None, Field(alias="organizationEid")] = None + + +class StartIntegrationReconnectSessionArgs(GeneratedModel): + """Arguments of the `startIntegrationReconnectSession` mutation. + + Mint a short-lived Nango Connect session token for repairing an existing, + broken connection for the given integration (e.g. "hubspot"), scoped to + the current organization. The frontend uses this token to open Nango's + hosted Connect UI popup. + """ + + integration: str | None = None + organization_eid: Annotated[str | None, Field(alias="organizationEid")] = None + + +class UpdateCastArgs(GeneratedModel): + """Arguments of the `updateCast` mutation. + + Update a `Cast` with new properties. This will create a new `CastVersion` and: + 1. If this being used as a Template, it will require you to publish the `Cast` via + the [`publishCast`]({{Mutations.publishCast}}) mutation + before it will go live. + 2. If the `Cast` is part of a Workflow, it will require you to publish the `Weld` via + the [`publishWeld`]({{Mutations.publishWeld}}) mutation + """ + + allowed_alias_ids: Annotated[ + list[str | None] | None, Field(alias="allowedAliasIds") + ] = None + config: Any | None = None + config_file: Annotated[Any | None, Field(alias="configFile")] = None + eid: str + feedback_message: Annotated[str | None, Field(alias="feedbackMessage")] = None + feedback_rating: Annotated[int | None, Field(alias="feedbackRating")] = None + file: Any | None = None + is_archived: Annotated[bool | None, Field(alias="isArchived")] = None + is_template: Annotated[bool | None, Field(alias="isTemplate")] = None + name: str | None = None + organization_eid: Annotated[str | None, Field(alias="organizationEid")] = None + title: str | None = None + version_number: Annotated[ + int | None, + Field( + alias="versionNumber", + description="Used to ensure that the Cast is still at the version that you believed it is.\nHelps to prevent performing an update without being aware that an update has already\noccurred in the meantime.", + ), + ] = None + + +class UpdateEtchFreeArgs(GeneratedModel): + """Arguments of the `updateEtchFree` mutation.""" + + cast_fields: Annotated[list[Any | None] | None, Field(alias="castFields")] = None + etch_template_eid: Annotated[str, Field(alias="etchTemplateEid")] + fields: list[Any | None] | None = None + is_editing: Annotated[bool | None, Field(alias="isEditing")] = None + name: str | None = None + owner_email: Annotated[str | None, Field(alias="ownerEmail")] = None + owner_name: Annotated[str | None, Field(alias="ownerName")] = None + signers: Any | None = None + token: str + + +class UpdateEtchPacketArgs(GeneratedModel): + """Arguments of the `updateEtchPacket` mutation.""" + + eid: str + is_archived: Annotated[bool | None, Field(alias="isArchived")] = None + merge_payloads: Annotated[ + bool | None, + Field( + alias="mergePayloads", + description="If `true` we treat the provided `payload` as a partial update to be merged with the previous payload.\nAny values in the `payload` that are set to `null` will be removed from the resulting merged payload.", + ), + ] = None + name: str | None = None + payload: Any | None = None + token: str | None = None + webhook_url: Annotated[str | None, Field(alias="webhookURL")] = None + + +class UpdateEtchTemplateArgs(GeneratedModel): + """Arguments of the `updateEtchTemplate` mutation.""" + + config: Any | None = None + eid: str + is_repeatable: Annotated[bool | None, Field(alias="isRepeatable")] = None + name: str | None = None + + +class UpdateForgeArgs(GeneratedModel): + """Arguments of the `updateForge` mutation. + + Update a `Forge` with new properties. This will create a new `ForgeVersion` and + will require you to publish the `Weld` via the [`publishWeld`]({{Mutations.publishWeld}}) mutation + before it will go live. + """ + + config: Any | None = None + config_file: Annotated[Any | None, Field(alias="configFile")] = None + eid: str + is_archived: Annotated[bool | None, Field(alias="isArchived")] = None + is_required: Annotated[bool | None, Field(alias="isRequired")] = None + name: str | None = None + organization_role: Annotated[str | None, Field(alias="organizationRole")] = None + slug: str | None = None + title: str | None = None + unauthenticated_auth_type: Annotated[ + str | None, Field(alias="unauthenticatedAuthType") + ] = None + version_number: Annotated[ + int | None, + Field( + alias="versionNumber", + description="Used to ensure that the Forge is still at the version that you believed it is.\nHelps to prevent performing an update without being aware that an update has already\noccurred in the meantime.", + ), + ] = None + + +class UpdateOrganizationArgs(GeneratedModel): + """Arguments of the `updateOrganization` mutation.""" + + billing_email: Annotated[str | None, Field(alias="billingEmail")] = None + config: Any | None = None + default_source_id: Annotated[str | None, Field(alias="defaultSourceId")] = None + etch_complete_email_recipients: Annotated[ + Any | None, Field(alias="etchCompleteEmailRecipients") + ] = None + logo: Any | None = None + name: str | None = None + organization_eid: Annotated[str | None, Field(alias="organizationEid")] = None + organization_slug: Annotated[str | None, Field(alias="organizationSlug")] = None + signature_provider: Annotated[str | None, Field(alias="signatureProvider")] = None + signer_complete_email_recipients: Annotated[ + Any | None, Field(alias="signerCompleteEmailRecipients") + ] = None + signer_options: Annotated[Any | None, Field(alias="signerOptions")] = None + signer_view_email_recipients: Annotated[ + Any | None, Field(alias="signerViewEmailRecipients") + ] = None + slug: str | None = None + stylesheet_url: Annotated[str | None, Field(alias="stylesheetURL")] = None + support_email: Annotated[str | None, Field(alias="supportEmail")] = None + weld_complete_email_enable_for_test: Annotated[ + bool | None, Field(alias="weldCompleteEmailEnableForTest") + ] = None + weld_complete_email_recipients: Annotated[ + Any | None, Field(alias="weldCompleteEmailRecipients") + ] = None + + +class UpdateOrganizationUserArgs(GeneratedModel): + """Arguments of the `updateOrganizationUser` mutation.""" + + external_id: Annotated[str | None, Field(alias="externalId")] = None + organization_eid: Annotated[str, Field(alias="organizationEid")] + role: str + user_eid: Annotated[str, Field(alias="userEid")] + + +class UpdateSignerArgs(GeneratedModel): + """Arguments of the `updateSigner` mutation. + + Update a `Signer`'s name and email address. A signer's name and email can + be updated as long as the signer has not completed signing or been skipped. + + Name and email for a signer can be updated after a previous signer + has already signed. Documents will be updated with the new signer + information. Only name and email fields that have been attached to a signer + will be updated. An invisible signature will be added to each updated + document indicating what changed and who changed it. + + This mutation can be used on signers in `EtchPackets` and in Workflows + (attached to `WeldData` objects). + """ + + activate_if_pending: Annotated[ + bool | None, + Field( + alias="activateIfPending", + description="If this is an Etch signer and they were created as `pending`, set this to `true` to activate them.", + ), + ] = None + alias_id: Annotated[ + str | None, + Field( + alias="aliasId", + description="Optionally provide a new or update an existing user-specified id for the `Signer`. This is specified as `id` in the [`createEtchPacket`]({{Mutations.createEtchPacket}})\nmutation. Note: This will only be updated if the `name` and/or the `email` are also changed.", + ), + ] = None + eid: Annotated[str, Field(description="The eid of the `Signer` to update.")] + email: Annotated[ + str | None, Field(description="The signer's new email address.") + ] = None + name: Annotated[str | None, Field(description="The signer's new name.")] = None + + +class UpdateSubmissionArgs(GeneratedModel): + """Arguments of the `updateSubmission` mutation.""" + + eid: str + is_excluded: Annotated[bool | None, Field(alias="isExcluded")] = None + + +class UpdateWebhookArgs(GeneratedModel): + """Arguments of the `updateWebhook` mutation.""" + + eid: str + url: str | None = None + + +class UpdateWeldArgs(GeneratedModel): + """Arguments of the `updateWeld` mutation. + + Update a `Weld` with new properties. This will create a new `WeldVersion` and + will require you to publish the `Weld` via the [`publishWeld`]({{Mutations.publishWeld}}) mutation + before it will go live. + """ + + config: Any | None = None + config_file: Annotated[Any | None, Field(alias="configFile")] = None + data_display_title: Annotated[Any | None, Field(alias="dataDisplayTitle")] = None + draft_step: Annotated[str | None, Field(alias="draftStep")] = None + eid: str + entry_button_copy_link: Annotated[ + bool | None, Field(alias="entryButtonCopyLink") + ] = None + entry_button_text: Annotated[str | None, Field(alias="entryButtonText")] = None + entry_forge_id: Annotated[int | None, Field(alias="entryForgeId")] = None + exclude_certificate_from_signer_downloads: Annotated[ + bool | None, Field(alias="excludeCertificateFromSignerDownloads") + ] = None + expire_after_days_complete: Annotated[ + int | None, Field(alias="expireAfterDaysComplete") + ] = None + expire_after_days_start: Annotated[ + int | None, Field(alias="expireAfterDaysStart") + ] = None + expires_at: Annotated[str | None, Field(alias="expiresAt")] = None + is_archived: Annotated[bool | None, Field(alias="isArchived")] = None + locked_description_existing: Annotated[ + str | None, Field(alias="lockedDescriptionExisting") + ] = None + locked_description_new: Annotated[ + str | None, Field(alias="lockedDescriptionNew") + ] = None + locked_title_existing: Annotated[str | None, Field(alias="lockedTitleExisting")] = ( + None + ) + locked_title_new: Annotated[str | None, Field(alias="lockedTitleNew")] = None + merge_pdfs: Annotated[bool | None, Field(alias="mergePDFs")] = None + name: str | None = None + organization_eid: Annotated[str | None, Field(alias="organizationEid")] = None + signature_email_body: Annotated[Any | None, Field(alias="signatureEmailBody")] = ( + None + ) + signature_email_subject: Annotated[ + Any | None, Field(alias="signatureEmailSubject") + ] = None + signature_mode: Annotated[str | None, Field(alias="signatureMode")] = None + signature_provider: Annotated[str | None, Field(alias="signatureProvider")] = None + signature_steps_mode: Annotated[str | None, Field(alias="signatureStepsMode")] = ( + None + ) + slug: str | None = None + steps: Any | None = None + version_number: Annotated[ + int | None, + Field( + alias="versionNumber", + description="Used to ensure that the Weld is still at the version that you believed it is.\nHelps to prevent performing an update without being aware that an update has already\noccurred in the meantime.", + ), + ] = None + visibility: str | None = None + weld_complete_email_enable_for_test: Annotated[ + bool | None, Field(alias="weldCompleteEmailEnableForTest") + ] = None + weld_complete_email_recipients: Annotated[ + Any | None, Field(alias="weldCompleteEmailRecipients") + ] = None + + +class UpdateWeldDataArgs(GeneratedModel): + """Arguments of the `updateWeldData` mutation.""" + + eid: str + is_archived: Annotated[bool | None, Field(alias="isArchived")] = None + is_expired: Annotated[bool | None, Field(alias="isExpired")] = None + is_test: Annotated[bool | None, Field(alias="isTest")] = None + pin: str | None = None + webhook_url: Annotated[str | None, Field(alias="webhookURL")] = None + + +class VoidDocumentGroupArgs(GeneratedModel): + """Arguments of the `voidDocumentGroup` mutation.""" + + eid: Annotated[str | None, Field(description="The DocumentGroup's `eid`")] = None + voided_reason: Annotated[str, Field(alias="voidedReason")] + weld_data_eid: Annotated[str | None, Field(alias="weldDataEid")] = None + + +__all__ = [ + "CastArgs", + "CreateCastArgs", + "CreateEtchPacketArgs", + "CreateForgeArgs", + "CreateSubmissionArgs", + "CreateWebhookActionArgs", + "CreateWebhookArgs", + "CreateWeldArgs", + "CreateWeldDataArgs", + "CurrentUserArgs", + "DestroySubmissionArgs", + "DisconnectDocusignArgs", + "DuplicateCastArgs", + "DuplicateWeldArgs", + "EtchPacketArgs", + "ExpireSessionTokenArgs", + "ExpireSignerTokensArgs", + "ForgeArgs", + "ForgeSubmitArgs", + "GenerateEmbedURLArgs", + "GenerateEtchSignURLArgs", + "IntegrationArgs", + "MergeWeldsArgs", + "NotifySignerArgs", + "OrganizationArgs", + "PublishCastArgs", + "PublishWeldArgs", + "RemoveEtchPacketArgs", + "RemoveWebhookActionArgs", + "RemoveWebhookArgs", + "RemoveWeldDataArgs", + "RetryWebhookLogArgs", + "SendEtchPacketArgs", + "SignerArgs", + "SkipSignerArgs", + "StartIntegrationConnectSessionArgs", + "StartIntegrationReconnectSessionArgs", + "SubmissionArgs", + "UpdateCastArgs", + "UpdateEtchFreeArgs", + "UpdateEtchPacketArgs", + "UpdateEtchTemplateArgs", + "UpdateForgeArgs", + "UpdateOrganizationArgs", + "UpdateOrganizationUserArgs", + "UpdateSignerArgs", + "UpdateSubmissionArgs", + "UpdateWebhookArgs", + "UpdateWeldArgs", + "UpdateWeldDataArgs", + "VoidDocumentGroupArgs", + "WeldArgs", + "WeldDataArgs", +] diff --git a/python_anvil/types/rest.py b/python_anvil/types/rest.py new file mode 100644 index 00000000..ecfc3cdc --- /dev/null +++ b/python_anvil/types/rest.py @@ -0,0 +1,700 @@ +# Generated by dev/generate_types.py from schema/openapi.json. Do not edit by hand. +# Run `make generate` to regenerate. + +from __future__ import annotations + +from typing import Annotated, Any, Literal + +from pydantic import ConfigDict, Field + +from python_anvil.types._base import GeneratedModel + + +class EtchFinishPageOptions(GeneratedModel): + """ + createEtchPacket's `finishPageOptions` argument: text for the page signers see when they're done. + """ + + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + complete_description: Annotated[str | None, Field(alias="completeDescription")] = ( + None + ) + complete_title: Annotated[str | None, Field(alias="completeTitle")] = None + incomplete_description: Annotated[ + str | None, Field(alias="incompleteDescription") + ] = None + incomplete_title: Annotated[str | None, Field(alias="incompleteTitle")] = None + signature_async_description: Annotated[ + str | None, Field(alias="signatureAsyncDescription") + ] = None + signature_async_title: Annotated[str | None, Field(alias="signatureAsyncTitle")] = ( + None + ) + signature_button_text: Annotated[str | None, Field(alias="signatureButtonText")] = ( + None + ) + signature_description: Annotated[ + str | None, Field(alias="signatureDescription") + ] = None + signature_title: Annotated[str | None, Field(alias="signatureTitle")] = None + signature_waiting_signers_after_description: Annotated[ + str | None, Field(alias="signatureWaitingSignersAfterDescription") + ] = None + signature_waiting_signers_after_title: Annotated[ + str | None, Field(alias="signatureWaitingSignersAfterTitle") + ] = None + signature_waiting_signers_before_description: Annotated[ + str | None, Field(alias="signatureWaitingSignersBeforeDescription") + ] = None + signature_waiting_signers_before_title: Annotated[ + str | None, Field(alias="signatureWaitingSignersBeforeTitle") + ] = None + zip_name: Annotated[str | None, Field(alias="zipName")] = None + + +class Style(GeneratedModel): + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + info_color: Annotated[str | None, Field(alias="infoColor")] = None + link_color: Annotated[str | None, Field(alias="linkColor")] = None + primary_color: Annotated[str | None, Field(alias="primaryColor")] = None + stylesheet_url: Annotated[str | None, Field(alias="stylesheetURL")] = None + success_color: Annotated[str | None, Field(alias="successColor")] = None + + +class EtchSignaturePageOptions(GeneratedModel): + """ + createEtchPacket's `signaturePageOptions` argument: text and styles for the signing page. + """ + + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + accept_button_text: Annotated[str | None, Field(alias="acceptButtonText")] = None + accept_description: Annotated[str | None, Field(alias="acceptDescription")] = None + accept_title: Annotated[str | None, Field(alias="acceptTitle")] = None + completed_button_text: Annotated[str | None, Field(alias="completedButtonText")] = ( + None + ) + description: str | None = None + draw_initials_button_text: Annotated[ + str | None, Field(alias="drawInitialsButtonText") + ] = None + draw_initials_description: Annotated[ + str | None, Field(alias="drawInitialsDescription") + ] = None + draw_initials_title: Annotated[str | None, Field(alias="drawInitialsTitle")] = None + draw_signature_button_text: Annotated[ + str | None, Field(alias="drawSignatureButtonText") + ] = None + draw_signature_description: Annotated[ + str | None, Field(alias="drawSignatureDescription") + ] = None + draw_signature_title: Annotated[str | None, Field(alias="drawSignatureTitle")] = ( + None + ) + error: str | None = None + initial_label: Annotated[str | None, Field(alias="initialLabel")] = None + not_current_user_error: Annotated[ + str | None, Field(alias="notCurrentUserError") + ] = None + review_button_text: Annotated[str | None, Field(alias="reviewButtonText")] = None + review_description: Annotated[str | None, Field(alias="reviewDescription")] = None + review_title: Annotated[str | None, Field(alias="reviewTitle")] = None + sign_button_text: Annotated[str | None, Field(alias="signButtonText")] = None + sign_consent_text: Annotated[str | None, Field(alias="signConsentText")] = None + sign_description: Annotated[str | None, Field(alias="signDescription")] = None + sign_description_completed: Annotated[ + str | None, Field(alias="signDescriptionCompleted") + ] = None + sign_each_completed_message: Annotated[ + str | None, Field(alias="signEachCompletedMessage") + ] = None + sign_each_consent_text: Annotated[ + str | None, Field(alias="signEachConsentText") + ] = None + sign_each_description: Annotated[str | None, Field(alias="signEachDescription")] = ( + None + ) + sign_each_error_message: Annotated[ + str | None, Field(alias="signEachErrorMessage") + ] = None + sign_each_finish_button_text: Annotated[ + str | None, Field(alias="signEachFinishButtonText") + ] = None + sign_each_next_button_text: Annotated[ + str | None, Field(alias="signEachNextButtonText") + ] = None + sign_each_not_ready_message: Annotated[ + str | None, Field(alias="signEachNotReadyMessage") + ] = None + sign_each_sign_all_text: Annotated[ + str | None, Field(alias="signEachSignAllText") + ] = None + sign_each_signing_message: Annotated[ + str | None, Field(alias="signEachSigningMessage") + ] = None + sign_each_start_button_text: Annotated[ + str | None, Field(alias="signEachStartButtonText") + ] = None + sign_each_submitting_message: Annotated[ + str | None, Field(alias="signEachSubmittingMessage") + ] = None + sign_title: Annotated[str | None, Field(alias="signTitle")] = None + signature_label: Annotated[str | None, Field(alias="signatureLabel")] = None + style: Style | None = None + title: str | None = None + + +class Fields(GeneratedModel): + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + field_id: Annotated[str, Field(alias="fieldId")] + file_id: Annotated[str, Field(alias="fileId")] + signature_image_vertical_justification: Annotated[ + Literal["dynamic", "bottom", "none"] | None, + Field(alias="signatureImageVerticalJustification"), + ] = None + + +class PayloadMap(GeneratedModel): + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + field_id: Annotated[str, Field(alias="fieldId")] + file_id: Annotated[str, Field(alias="fileId")] + + +class Fields1(GeneratedModel): + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + description: str | None = None + help_tip: Annotated[str | None, Field(alias="helpTip")] = None + ignore_when_filled: Annotated[bool | None, Field(alias="ignoreWhenFilled")] = None + kind: Literal["form"] + label: str | None = None + optional: bool | None = None + options: dict[str, Any] | None = None + payload_maps: Annotated[list[PayloadMap], Field(alias="payloadMaps", min_length=1)] + placeholder: str | None = None + + +class EtchSigner(GeneratedModel): + """ + A signer in createEtchPacket's `signers` argument: who signs, which fields they sign, and how. + """ + + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + accept_each_field: Annotated[bool | None, Field(alias="acceptEachField")] = None + access_code: Annotated[str | None, Field(alias="accessCode")] = None + add_timestamp_to_signatures: Annotated[ + bool | None, Field(alias="addTimestampToSignatures") + ] = None + add_uuid_to_signatures: Annotated[ + bool | None, Field(alias="addUuidToSignatures") + ] = None + allow_email_change: Annotated[bool | None, Field(alias="allowEmailChange")] = None + allow_name_change: Annotated[bool | None, Field(alias="allowNameChange")] = None + email: str | None = None + enable_emails: Annotated[ + bool | list[Literal["etchComplete"]] | None, Field(alias="enableEmails") + ] = None + fields: list[Fields | Fields1] | None = None + fields_are_ordered: Annotated[bool | None, Field(alias="fieldsAreOrdered")] = None + id: str + id_check_phone: Annotated[str | None, Field(alias="idCheckPhone")] = None + is_pending: Annotated[bool | None, Field(alias="isPending")] = None + name: str | None = None + redirect_url: Annotated[str | None, Field(alias="redirectURL")] = None + require_email_on_load: Annotated[bool | None, Field(alias="requireEmailOnLoad")] = ( + None + ) + require_id_check: Annotated[ + bool | Literal["kb", "sms", "smskb", "idsms"] | None, + Field(alias="requireIdCheck"), + ] = None + require_id_check_in_test: Annotated[ + bool | None, Field(alias="requireIdCheckInTest") + ] = None + require_name_on_load: Annotated[bool | None, Field(alias="requireNameOnLoad")] = ( + None + ) + routing_order: Annotated[int | None, Field(alias="routingOrder", ge=1)] = None + show_finish_signing_button: Annotated[ + bool | None, Field(alias="showFinishSigningButton") + ] = None + signature_experience: Annotated[ + Literal[ + "signBeforeReviewing", + "signWhileReviewing", + "acknowledgeOnly", + "formCompletionOnly", + ] + | None, + Field(alias="signatureExperience"), + ] = None + signature_image_vertical_justification_default: Annotated[ + Literal["dynamic", "bottom", "none"] | None, + Field(alias="signatureImageVerticalJustificationDefault"), + ] = None + signature_mode: Annotated[ + Literal["draw", "text"] | None, Field(alias="signatureMode") + ] = None + signer_type: Annotated[ + Literal["email", "embedded", "in-person"] | None, Field(alias="signerType") + ] = None + token_valid_for_minutes: Annotated[ + int | None, Field(alias="tokenValidForMinutes", ge=1, le=86400) + ] = None + token_valid_until: Annotated[str | None, Field(alias="tokenValidUntil")] = None + + +class Data(GeneratedModel): + """ + A value object with optional per-field styling overrides. + """ + + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + value: str | float | bool | None = None + + +class Data1(GeneratedModel): + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + value: str | float | bool | None = None + + +class FillPdfRequest(GeneratedModel): + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + border_color: Annotated[ + str | None, + Field( + alias="borderColor", + description="Default border color for field bounding boxes, as a CSS hex color.", + ), + ] = None + data: Annotated[ + dict[str, str | float | bool | Data] + | list[dict[str, str | float | bool | Data1]], + Field( + description="The field data to fill into the template. Can be a single object keyed by\nfield ID or alias, or an array of objects for multi-page / repeating fills.\nEach value can be a primitive (string, number, boolean), or a value object\nwith a `value` key alongside styling overrides.\n" + ), + ] + default_checkbox_character: Annotated[ + str | None, + Field( + alias="defaultCheckboxCharacter", + description="The character used to represent a checked checkbox.", + ), + ] = None + default_make_checkbox_square: Annotated[ + bool | None, + Field( + alias="defaultMakeCheckboxSquare", + description="When true, checkbox fields are rendered as squares instead of the default shape.", + ), + ] = None + default_read_only: Annotated[ + bool | None, + Field( + alias="defaultReadOnly", + description="Controls whether interactive AcroForm fields default to read-only.\nOnly applicable when `useInteractiveFields` is true. Defaults to false\nwhen `useInteractiveFields` is enabled.\n", + ), + ] = None + draw_debug_boxes: Annotated[ + bool | None, + Field( + alias="drawDebugBoxes", + description="When true, draws visible bounding boxes around each field for debugging layout issues.", + ), + ] = None + font_family: Annotated[ + str | None, + Field( + alias="fontFamily", description="Default font family name for all fields." + ), + ] = None + font_size: Annotated[ + int | None, + Field( + alias="fontSize", + description="Default font size in points for all fields.", + ge=5, + le=30, + ), + ] = None + output_format: Annotated[ + Literal["pdf", "url"] | None, + Field( + alias="outputFormat", + description="Controls the response format. When set to `url`, the filled PDF is\nuploaded to cloud storage and a JSON response with a short-lived signed\nURL is returned instead of the binary PDF. The signed URL expires 5\nminutes after generation. Defaults to `pdf` (binary PDF response).\n", + ), + ] = "pdf" + strip_annotations: Annotated[ + bool | None, + Field( + alias="stripAnnotations", + description="When true, removes all PDF annotations from the output.", + ), + ] = None + text_color: Annotated[ + str | None, + Field( + alias="textColor", + description="Default text color for all fields, as a CSS hex color (e.g. `#FF0000`).", + ), + ] = None + title: Annotated[ + str | None, + Field( + description="Custom title for the generated PDF. Overrides the template's default title." + ), + ] = None + use_interactive_fields: Annotated[ + bool | None, + Field( + alias="useInteractiveFields", + description="When true, the filled PDF will contain interactive AcroForm fields\nthat can be edited by the recipient in a PDF viewer. Requires a paid plan.\n", + ), + ] = None + vertical_alignment: Annotated[ + Literal["top", "middle", "bottom"] | None, + Field( + alias="verticalAlignment", + description="Default vertical alignment for text within fields.", + ), + ] = None + + +class Data2(GeneratedModel): + """ + The HTML content and optional CSS to render into a PDF. + """ + + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + css: Annotated[ + str | None, + Field( + description="Optional CSS stylesheet applied to the HTML content during rendering." + ), + ] = None + html: Annotated[ + str, + Field( + description="The HTML markup to render. This is the full body content of the PDF." + ), + ] + + +class ColumnOption(GeneratedModel): + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + align: Annotated[ + Literal["left", "center", "right"] | None, + Field(description="Horizontal text alignment for this column."), + ] = None + width: Annotated[ + str | None, + Field( + description="Column width as a percentage or pixel value (e.g. `25%`, `100px`)." + ), + ] = None + + +class Table(GeneratedModel): + """ + A table rendered inline in the PDF. + """ + + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + column_gridlines: Annotated[ + bool | None, + Field( + alias="columnGridlines", + description="When true, vertical gridlines are drawn between columns.", + ), + ] = None + column_options: Annotated[ + list[ColumnOption] | None, + Field(alias="columnOptions", description="Per-column display options."), + ] = None + first_row_headers: Annotated[ + bool | None, + Field( + alias="firstRowHeaders", + description="When true, the first row is rendered as a header row with bold styling.", + ), + ] = None + row_gridlines: Annotated[ + bool | None, + Field( + alias="rowGridlines", + description="When true, horizontal gridlines are drawn between rows.", + ), + ] = None + rows: Annotated[ + list[list[str]], + Field( + description="Table data as a 2D array of strings (array of rows, each row is an array of cell values)." + ), + ] + vertical_align: Annotated[ + Literal["top", "center", "bottom"] | None, + Field(alias="verticalAlign", description="Vertical alignment of cell content."), + ] = None + + +class Datum(GeneratedModel): + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + content: Annotated[ + str | None, + Field( + description="Body text content, rendered as a paragraph. Supports Markdown formatting." + ), + ] = None + font_family: Annotated[ + str | None, + Field( + alias="fontFamily", + description="Font family override for this content block.", + ), + ] = None + font_size: Annotated[ + int | None, + Field( + alias="fontSize", + description="Font size override for this content block.", + ge=5, + le=30, + ), + ] = None + heading: Annotated[ + str | None, + Field(description="A section heading rendered in a larger, bold font."), + ] = None + label: Annotated[ + str | None, + Field( + description="A label rendered in bold, typically used for key-value pairs." + ), + ] = None + table: Annotated[ + Table | None, Field(description="A table rendered inline in the PDF.") + ] = None + text_color: Annotated[ + str | None, + Field( + alias="textColor", + description="Text color override for this block, as a CSS hex color.", + ), + ] = None + + +class Logo(GeneratedModel): + """ + A logo image to display at the top of the PDF. Set to `false` to + explicitly disable any default logo. Provide an object with a + base64-encoded or URL image source to include a logo. + + """ + + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + max_height: Annotated[ + str | float | None, + Field(alias="maxHeight", description="Maximum height of the logo in pixels."), + ] = None + max_width: Annotated[ + str | float | None, + Field(alias="maxWidth", description="Maximum width of the logo in pixels."), + ] = None + src: Annotated[ + str, + Field(description="Image source: a base64-encoded data URI or an HTTPS URL."), + ] + + +class PageOptions(GeneratedModel): + """ + Custom page dimensions and margins for the generated PDF. + """ + + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + height: Annotated[ + str | None, Field(description="Page height with unit (e.g. `11in`, `297mm`).") + ] = None + margin: Annotated[ + str | None, + Field( + description="Shorthand margin for all sides (e.g. `0.5in`, `10mm 20mm`)." + ), + ] = None + margin_bottom: Annotated[ + str | None, Field(alias="marginBottom", description="Bottom margin with unit.") + ] = None + margin_left: Annotated[ + str | None, Field(alias="marginLeft", description="Left margin with unit.") + ] = None + margin_right: Annotated[ + str | None, Field(alias="marginRight", description="Right margin with unit.") + ] = None + margin_top: Annotated[ + str | None, Field(alias="marginTop", description="Top margin with unit.") + ] = None + page_count: Annotated[ + Literal[ + "topLeft", + "topRight", + "topCenter", + "bottomLeft", + "bottomRight", + "bottomCenter", + ] + | None, + Field( + alias="pageCount", + description="Position to render page numbers on each page.", + ), + ] = None + width: Annotated[ + str | None, Field(description="Page width with unit (e.g. `8.5in`, `210mm`).") + ] = None + + +class GenerateHtmlPdfRequest(GeneratedModel): + """ + Generate a PDF by rendering raw HTML and optional CSS. + """ + + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + data: Annotated[ + Data2, + Field(description="The HTML content and optional CSS to render into a PDF."), + ] + output_format: Annotated[ + Literal["pdf", "url"] | None, + Field( + alias="outputFormat", + description="Controls the response format. When set to `url`, the generated PDF is\nuploaded to cloud storage and a JSON response with a short-lived signed\nURL is returned instead of the binary PDF. The signed URL expires 5\nminutes after generation. Defaults to `pdf` (binary PDF response).\n", + ), + ] = "pdf" + page: PageOptions | None = None + title: Annotated[ + str | None, + Field( + description="The document title. Used as the PDF metadata title and in the output filename." + ), + ] = None + type: Annotated[ + Literal["html"] | None, + Field(description="Set to `html` to explicitly select HTML-to-PDF rendering."), + ] = None + + +class GenerateMarkdownPdfRequest(GeneratedModel): + """ + Generate a PDF from structured Markdown-style content blocks. + Each item in the `data` array represents a content section that + can contain headings, labeled values, body text, or tables. + + """ + + model_config = ConfigDict( + extra="allow", + populate_by_name=True, + ) + data: Annotated[ + list[Datum], + Field( + description="An array of content blocks that make up the PDF body. Each block\ncan contain one or more of: `heading`, `label`, `content`, `table`.\n", + min_length=1, + ), + ] + font_family: Annotated[ + str | None, + Field(alias="fontFamily", description="Default font family for the document."), + ] = None + font_size: Annotated[ + int | None, + Field( + alias="fontSize", + description="Default font size in points for the document.", + ge=5, + le=30, + ), + ] = None + include_timestamp: Annotated[ + bool | None, + Field( + alias="includeTimestamp", + description="When true, a timestamp is printed on the PDF showing when it was generated.", + ), + ] = None + logo: Annotated[ + Literal[False] | Logo | None, + Field( + description="A logo image to display at the top of the PDF. Set to `false` to\nexplicitly disable any default logo. Provide an object with a\nbase64-encoded or URL image source to include a logo.\n" + ), + ] = None + output_format: Annotated[ + Literal["pdf", "url"] | None, + Field( + alias="outputFormat", + description="Controls the response format. When set to `url`, the generated PDF is\nuploaded to cloud storage and a JSON response with a short-lived signed\nURL is returned instead of the binary PDF. The signed URL expires 5\nminutes after generation. Defaults to `pdf` (binary PDF response).\n", + ), + ] = "pdf" + page: PageOptions | None = None + text_color: Annotated[ + str | None, + Field( + alias="textColor", + description="Default text color for the document, as a CSS hex color (e.g. `#333333`).", + ), + ] = None + title: Annotated[ + str | None, + Field( + description="The document title. Used as the PDF metadata title and in the output filename." + ), + ] = None diff --git a/python_anvil/utils.py b/python_anvil/utils.py index e80511b3..407d4a04 100644 --- a/python_anvil/utils.py +++ b/python_anvil/utils.py @@ -41,4 +41,9 @@ def remove_empty_items(dict_obj: dict): def camel_to_snake(name: str) -> str: - return re.sub(r"(? webhook_url.""" + # An acronym followed by a word: "URLFor" -> "URL_For". Two lowercase letters + # or more make a word, so a plural "s" stays with its acronym: "mergePDFs". + name = re.sub(r"([A-Z]+)([A-Z][a-z]{2,})", r"\1_\2", name) + name = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", name) + return name.lower() diff --git a/schema/openapi.json b/schema/openapi.json new file mode 100644 index 00000000..828fe5a1 --- /dev/null +++ b/schema/openapi.json @@ -0,0 +1,1086 @@ +{ + "components": { + "schemas": { + "EtchFinishPageOptions": { + "additionalProperties": false, + "description": "createEtchPacket's `finishPageOptions` argument: text for the page signers see when they're done.", + "properties": { + "completeDescription": { + "type": "string" + }, + "completeTitle": { + "type": "string" + }, + "incompleteDescription": { + "type": "string" + }, + "incompleteTitle": { + "type": "string" + }, + "signatureAsyncDescription": { + "type": "string" + }, + "signatureAsyncTitle": { + "type": "string" + }, + "signatureButtonText": { + "type": "string" + }, + "signatureDescription": { + "type": "string" + }, + "signatureTitle": { + "type": "string" + }, + "signatureWaitingSignersAfterDescription": { + "type": "string" + }, + "signatureWaitingSignersAfterTitle": { + "type": "string" + }, + "signatureWaitingSignersBeforeDescription": { + "type": "string" + }, + "signatureWaitingSignersBeforeTitle": { + "type": "string" + }, + "zipName": { + "type": "string" + } + }, + "type": "object" + }, + "EtchSignaturePageOptions": { + "additionalProperties": false, + "description": "createEtchPacket's `signaturePageOptions` argument: text and styles for the signing page.", + "properties": { + "acceptButtonText": { + "type": "string" + }, + "acceptDescription": { + "type": "string" + }, + "acceptTitle": { + "type": "string" + }, + "completedButtonText": { + "type": "string" + }, + "description": { + "type": "string" + }, + "drawInitialsButtonText": { + "type": "string" + }, + "drawInitialsDescription": { + "type": "string" + }, + "drawInitialsTitle": { + "type": "string" + }, + "drawSignatureButtonText": { + "type": "string" + }, + "drawSignatureDescription": { + "type": "string" + }, + "drawSignatureTitle": { + "type": "string" + }, + "error": { + "type": "string" + }, + "initialLabel": { + "type": "string" + }, + "notCurrentUserError": { + "type": "string" + }, + "reviewButtonText": { + "type": "string" + }, + "reviewDescription": { + "type": "string" + }, + "reviewTitle": { + "type": "string" + }, + "signButtonText": { + "type": "string" + }, + "signConsentText": { + "type": "string" + }, + "signDescription": { + "type": "string" + }, + "signDescriptionCompleted": { + "type": "string" + }, + "signEachCompletedMessage": { + "type": "string" + }, + "signEachConsentText": { + "type": "string" + }, + "signEachDescription": { + "type": "string" + }, + "signEachErrorMessage": { + "type": "string" + }, + "signEachFinishButtonText": { + "type": "string" + }, + "signEachNextButtonText": { + "type": "string" + }, + "signEachNotReadyMessage": { + "type": "string" + }, + "signEachSignAllText": { + "type": "string" + }, + "signEachSigningMessage": { + "type": "string" + }, + "signEachStartButtonText": { + "type": "string" + }, + "signEachSubmittingMessage": { + "type": "string" + }, + "signTitle": { + "type": "string" + }, + "signatureLabel": { + "type": "string" + }, + "style": { + "properties": { + "infoColor": { + "format": "hexColor", + "type": "string" + }, + "linkColor": { + "format": "hexColor", + "type": "string" + }, + "primaryColor": { + "format": "hexColor", + "type": "string" + }, + "stylesheetURL": { + "format": "publicUri", + "type": "string" + }, + "successColor": { + "format": "hexColor", + "type": "string" + } + }, + "type": "object" + }, + "title": { + "type": "string" + } + }, + "type": "object" + }, + "EtchSigner": { + "additionalProperties": false, + "description": "A signer in createEtchPacket's `signers` argument: who signs, which fields they sign, and how.", + "properties": { + "acceptEachField": { + "type": "boolean" + }, + "accessCode": { + "type": "string" + }, + "addTimestampToSignatures": { + "type": "boolean" + }, + "addUuidToSignatures": { + "type": "boolean" + }, + "allowEmailChange": { + "type": "boolean" + }, + "allowNameChange": { + "type": "boolean" + }, + "email": { + "format": "email", + "type": "string" + }, + "enableEmails": { + "oneOf": [ + { + "type": "boolean" + }, + { + "items": { + "enum": [ + "etchComplete" + ], + "type": "string" + }, + "type": "array" + } + ] + }, + "fields": { + "items": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "fieldId": { + "type": "string" + }, + "fileId": { + "type": "string" + }, + "signatureImageVerticalJustification": { + "enum": [ + "dynamic", + "bottom", + "none" + ], + "type": "string" + } + }, + "required": [ + "fileId", + "fieldId" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "description": { + "type": "string" + }, + "helpTip": { + "type": "string" + }, + "ignoreWhenFilled": { + "type": "boolean" + }, + "kind": { + "enum": [ + "form" + ], + "type": "string" + }, + "label": { + "type": "string" + }, + "optional": { + "type": "boolean" + }, + "options": { + "additionalProperties": true, + "type": "object" + }, + "payloadMaps": { + "items": { + "properties": { + "fieldId": { + "type": "string" + }, + "fileId": { + "type": "string" + } + }, + "required": [ + "fileId", + "fieldId" + ], + "type": "object" + }, + "minItems": 1, + "type": "array", + "uniqueItems": true + }, + "placeholder": { + "type": "string" + } + }, + "required": [ + "payloadMaps", + "kind" + ], + "type": "object" + } + ] + }, + "type": "array" + }, + "fieldsAreOrdered": { + "type": "boolean" + }, + "id": { + "type": "string" + }, + "idCheckPhone": { + "format": "phoneNumber", + "type": "string" + }, + "isPending": { + "type": "boolean" + }, + "name": { + "format": "displayableShortText", + "type": "string" + }, + "redirectURL": { + "format": "redirectURL", + "type": "string" + }, + "requireEmailOnLoad": { + "type": "boolean" + }, + "requireIdCheck": { + "oneOf": [ + { + "type": "boolean" + }, + { + "enum": [ + "kb", + "sms", + "smskb", + "idsms" + ], + "type": "string" + } + ] + }, + "requireIdCheckInTest": { + "type": "boolean" + }, + "requireNameOnLoad": { + "type": "boolean" + }, + "routingOrder": { + "minimum": 1, + "type": "integer" + }, + "showFinishSigningButton": { + "type": "boolean" + }, + "signatureExperience": { + "enum": [ + "signBeforeReviewing", + "signWhileReviewing", + "acknowledgeOnly", + "formCompletionOnly" + ], + "type": "string" + }, + "signatureImageVerticalJustificationDefault": { + "enum": [ + "dynamic", + "bottom", + "none" + ], + "type": "string" + }, + "signatureMode": { + "enum": [ + "draw", + "text" + ], + "type": "string" + }, + "signerType": { + "enum": [ + "email", + "embedded", + "in-person" + ], + "type": "string" + }, + "tokenValidForMinutes": { + "maximum": 86400, + "minimum": 1, + "type": "integer" + }, + "tokenValidUntil": { + "format": "date-time", + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "FillPdfRequest": { + "properties": { + "borderColor": { + "description": "Default border color for field bounding boxes, as a CSS hex color.", + "type": "string" + }, + "data": { + "description": "The field data to fill into the template. Can be a single object keyed by\nfield ID or alias, or an array of objects for multi-page / repeating fills.\nEach value can be a primitive (string, number, boolean), or a value object\nwith a `value` key alongside styling overrides.\n", + "oneOf": [ + { + "additionalProperties": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "description": "A value object with optional per-field styling overrides.", + "properties": { + "value": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + } + }, + "type": "object" + } + ] + }, + "type": "object" + }, + { + "items": { + "additionalProperties": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "properties": { + "value": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + } + }, + "type": "object" + } + ] + }, + "type": "object" + }, + "type": "array" + } + ] + }, + "defaultCheckboxCharacter": { + "description": "The character used to represent a checked checkbox.", + "type": "string" + }, + "defaultMakeCheckboxSquare": { + "description": "When true, checkbox fields are rendered as squares instead of the default shape.", + "type": "boolean" + }, + "defaultReadOnly": { + "description": "Controls whether interactive AcroForm fields default to read-only.\nOnly applicable when `useInteractiveFields` is true. Defaults to false\nwhen `useInteractiveFields` is enabled.\n", + "type": "boolean" + }, + "drawDebugBoxes": { + "description": "When true, draws visible bounding boxes around each field for debugging layout issues.", + "type": "boolean" + }, + "fontFamily": { + "description": "Default font family name for all fields.", + "type": "string" + }, + "fontSize": { + "description": "Default font size in points for all fields.", + "maximum": 30, + "minimum": 5, + "type": "integer" + }, + "outputFormat": { + "default": "pdf", + "description": "Controls the response format. When set to `url`, the filled PDF is\nuploaded to cloud storage and a JSON response with a short-lived signed\nURL is returned instead of the binary PDF. The signed URL expires 5\nminutes after generation. Defaults to `pdf` (binary PDF response).\n", + "enum": [ + "pdf", + "url" + ], + "type": "string" + }, + "stripAnnotations": { + "description": "When true, removes all PDF annotations from the output.", + "type": "boolean" + }, + "textColor": { + "description": "Default text color for all fields, as a CSS hex color (e.g. `#FF0000`).", + "type": "string" + }, + "title": { + "description": "Custom title for the generated PDF. Overrides the template's default title.", + "type": "string" + }, + "useInteractiveFields": { + "description": "When true, the filled PDF will contain interactive AcroForm fields\nthat can be edited by the recipient in a PDF viewer. Requires a paid plan.\n", + "type": "boolean" + }, + "verticalAlignment": { + "description": "Default vertical alignment for text within fields.", + "enum": [ + "top", + "middle", + "bottom" + ], + "type": "string" + } + }, + "required": [ + "data" + ], + "type": "object" + }, + "GenerateHtmlPdfRequest": { + "description": "Generate a PDF by rendering raw HTML and optional CSS.", + "properties": { + "data": { + "description": "The HTML content and optional CSS to render into a PDF.", + "properties": { + "css": { + "description": "Optional CSS stylesheet applied to the HTML content during rendering.", + "type": "string" + }, + "html": { + "description": "The HTML markup to render. This is the full body content of the PDF.", + "type": "string" + } + }, + "required": [ + "html" + ], + "type": "object" + }, + "outputFormat": { + "default": "pdf", + "description": "Controls the response format. When set to `url`, the generated PDF is\nuploaded to cloud storage and a JSON response with a short-lived signed\nURL is returned instead of the binary PDF. The signed URL expires 5\nminutes after generation. Defaults to `pdf` (binary PDF response).\n", + "enum": [ + "pdf", + "url" + ], + "type": "string" + }, + "page": { + "$ref": "#/components/schemas/PageOptions" + }, + "title": { + "description": "The document title. Used as the PDF metadata title and in the output filename.", + "type": "string" + }, + "type": { + "description": "Set to `html` to explicitly select HTML-to-PDF rendering.", + "enum": [ + "html" + ], + "type": "string" + } + }, + "required": [ + "data" + ], + "type": "object" + }, + "GenerateMarkdownPdfRequest": { + "description": "Generate a PDF from structured Markdown-style content blocks.\nEach item in the `data` array represents a content section that\ncan contain headings, labeled values, body text, or tables.\n", + "properties": { + "data": { + "description": "An array of content blocks that make up the PDF body. Each block\ncan contain one or more of: `heading`, `label`, `content`, `table`.\n", + "items": { + "properties": { + "content": { + "description": "Body text content, rendered as a paragraph. Supports Markdown formatting.", + "type": "string" + }, + "fontFamily": { + "description": "Font family override for this content block.", + "type": "string" + }, + "fontSize": { + "description": "Font size override for this content block.", + "maximum": 30, + "minimum": 5, + "type": "integer" + }, + "heading": { + "description": "A section heading rendered in a larger, bold font.", + "type": "string" + }, + "label": { + "description": "A label rendered in bold, typically used for key-value pairs.", + "type": "string" + }, + "table": { + "description": "A table rendered inline in the PDF.", + "properties": { + "columnGridlines": { + "description": "When true, vertical gridlines are drawn between columns.", + "type": "boolean" + }, + "columnOptions": { + "description": "Per-column display options.", + "items": { + "properties": { + "align": { + "description": "Horizontal text alignment for this column.", + "enum": [ + "left", + "center", + "right" + ], + "type": "string" + }, + "width": { + "description": "Column width as a percentage or pixel value (e.g. `25%`, `100px`).", + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + }, + "firstRowHeaders": { + "description": "When true, the first row is rendered as a header row with bold styling.", + "type": "boolean" + }, + "rowGridlines": { + "description": "When true, horizontal gridlines are drawn between rows.", + "type": "boolean" + }, + "rows": { + "description": "Table data as a 2D array of strings (array of rows, each row is an array of cell values).", + "items": { + "items": { + "type": "string" + }, + "type": "array" + }, + "type": "array" + }, + "verticalAlign": { + "description": "Vertical alignment of cell content.", + "enum": [ + "top", + "center", + "bottom" + ], + "type": "string" + } + }, + "required": [ + "rows" + ], + "type": "object" + }, + "textColor": { + "description": "Text color override for this block, as a CSS hex color.", + "type": "string" + } + }, + "type": "object" + }, + "minItems": 1, + "type": "array" + }, + "fontFamily": { + "description": "Default font family for the document.", + "type": "string" + }, + "fontSize": { + "description": "Default font size in points for the document.", + "maximum": 30, + "minimum": 5, + "type": "integer" + }, + "includeTimestamp": { + "description": "When true, a timestamp is printed on the PDF showing when it was generated.", + "type": "boolean" + }, + "logo": { + "description": "A logo image to display at the top of the PDF. Set to `false` to\nexplicitly disable any default logo. Provide an object with a\nbase64-encoded or URL image source to include a logo.\n", + "oneOf": [ + { + "enum": [ + false + ], + "type": "boolean" + }, + { + "properties": { + "maxHeight": { + "description": "Maximum height of the logo in pixels.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + } + ] + }, + "maxWidth": { + "description": "Maximum width of the logo in pixels.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + } + ] + }, + "src": { + "description": "Image source: a base64-encoded data URI or an HTTPS URL.", + "type": "string" + } + }, + "required": [ + "src" + ], + "type": "object" + } + ] + }, + "outputFormat": { + "default": "pdf", + "description": "Controls the response format. When set to `url`, the generated PDF is\nuploaded to cloud storage and a JSON response with a short-lived signed\nURL is returned instead of the binary PDF. The signed URL expires 5\nminutes after generation. Defaults to `pdf` (binary PDF response).\n", + "enum": [ + "pdf", + "url" + ], + "type": "string" + }, + "page": { + "$ref": "#/components/schemas/PageOptions" + }, + "textColor": { + "description": "Default text color for the document, as a CSS hex color (e.g. `#333333`).", + "type": "string" + }, + "title": { + "description": "The document title. Used as the PDF metadata title and in the output filename.", + "type": "string" + } + }, + "required": [ + "data" + ], + "type": "object" + }, + "PageOptions": { + "description": "Custom page dimensions and margins for the generated PDF.", + "properties": { + "height": { + "description": "Page height with unit (e.g. `11in`, `297mm`).", + "type": "string" + }, + "margin": { + "description": "Shorthand margin for all sides (e.g. `0.5in`, `10mm 20mm`).", + "type": "string" + }, + "marginBottom": { + "description": "Bottom margin with unit.", + "type": "string" + }, + "marginLeft": { + "description": "Left margin with unit.", + "type": "string" + }, + "marginRight": { + "description": "Right margin with unit.", + "type": "string" + }, + "marginTop": { + "description": "Top margin with unit.", + "type": "string" + }, + "pageCount": { + "description": "Position to render page numbers on each page.", + "enum": [ + "topLeft", + "topRight", + "topCenter", + "bottomLeft", + "bottomRight", + "bottomCenter" + ], + "type": "string" + }, + "width": { + "description": "Page width with unit (e.g. `8.5in`, `210mm`).", + "type": "string" + } + }, + "type": "object" + } + }, + "securitySchemes": { + "basicAuth": { + "description": "Use your API key as the username with an empty password.", + "scheme": "basic", + "type": "http" + }, + "oauth2": { + "flows": {}, + "type": "oauth2" + } + } + }, + "info": { + "description": "REST APIs for Anvil's document automation platform. Includes PDF filling, PDF generation, and more.", + "title": "Anvil API", + "version": "1.0.0" + }, + "openapi": "3.0.0", + "paths": { + "/api/document-group/{documentGroupEid}.zip": { + "get": { + "description": "Downloads every file in a document group as a single zip archive: for\nexample, the signed documents of an Etch signature packet, from its\n`documentGroup.eid`.\n\nInstead of streaming the zip, the response may be a `302` redirect to\na short-lived signed URL for it. Follow redirects to get the file.\n\nRequires API key (Basic auth) or OAuth authentication. Public OAuth\nclients need the `mcp:full` scope; no narrower scope covers this route.\n", + "operationId": "downloadDocuments", + "parameters": [ + { + "description": "The EID of the document group, e.g. an Etch packet's `documentGroup.eid`.", + "in": "path", + "name": "documentGroupEid", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/zip": { + "schema": { + "format": "binary", + "type": "string" + } + } + }, + "description": "The zip archive. `Content-Disposition` names it after the document\ngroup's parent object, such as the Etch packet.\n" + }, + "302": { + "description": "Redirect to a short-lived signed URL for the zip." + }, + "401": { + "description": "Authentication required \u2014 API key or OAuth token is missing or invalid." + }, + "403": { + "description": "The OAuth token belongs to a public client without a scope that covers this route." + }, + "404": { + "description": "Document group not found \u2014 no document group exists with the given EID, or the authenticated user lacks access to it." + } + }, + "security": [ + { + "basicAuth": [] + }, + { + "oauth2": [] + } + ], + "summary": "Download a document group's files as a zip", + "tags": [ + "Documents" + ] + } + }, + "/api/v1/fill/{castEid}.pdf": { + "post": { + "description": "Fills a PDF template (Cast) with the provided data and returns the resulting PDF.\n\nTemplates are created and managed in the Anvil dashboard or via the GraphQL `createCast` mutation.\nEach template defines named fields (text, date, checkbox, signature, etc.) that can be populated via\nthis endpoint. The field data can be supplied as a flat object keyed by field ID\nor alias, or as an array of objects for multi-page repeating templates.\n\nSupports both JSON and multipart/form-data request bodies. When using\nmultipart/form-data, string values for `fontSize` and `page` are automatically\nparsed.\n\nRequires API key (Basic auth) or OAuth authentication. Development API keys\nproduce watermarked PDFs; production keys produce clean PDFs and count toward\nyour plan's usage.\n", + "operationId": "fillPdf", + "parameters": [ + { + "description": "The EID (external identifier) of the PDF template (Cast) to fill.", + "in": "path", + "name": "castEid", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "The version number of the template to use. If omitted, the latest\npublished version is used. Use this to pin fills to a specific\ntemplate revision.\n", + "in": "query", + "name": "versionNumber", + "required": false, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FillPdfRequest" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/FillPdfRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/pdf": { + "schema": { + "format": "binary", + "type": "string" + } + } + }, + "description": "The filled PDF document." + }, + "400": { + "description": "Validation error \u2014 invalid or missing fields in the request body." + }, + "401": { + "description": "Authentication required \u2014 API key or OAuth token is missing or invalid." + }, + "404": { + "description": "Template not found \u2014 no Cast exists with the given EID, or the authenticated user lacks access." + }, + "415": { + "description": "Unsupported media type \u2014 Content-Type must be application/json or multipart/form-data." + }, + "429": { + "description": "Concurrency limit exceeded \u2014 too many simultaneous requests on a development API key." + } + }, + "security": [ + { + "basicAuth": [] + }, + { + "oauth2": [] + } + ], + "summary": "Fill a PDF template", + "tags": [ + "PDF" + ] + } + }, + "/api/v1/generate-pdf": { + "post": { + "description": "Generates a PDF document from either Markdown content or raw HTML/CSS.\nThe generation mode is determined automatically: if the request body\ncontains `type: \"html\"` or the `data` property includes an `html` key,\nHTML-to-PDF rendering is used; otherwise, Markdown-to-PDF rendering is used.\n\n**Markdown mode** accepts structured content blocks (headings, labels,\nbody text, and tables) and renders them into a styled PDF with optional\nlogo, timestamp, and custom page dimensions.\n\n**HTML mode** accepts raw HTML and optional CSS, rendering them directly\ninto a PDF with optional custom page dimensions.\n\nSupports both JSON and multipart/form-data request bodies. Requires API\nkey (Basic auth) or OAuth authentication.\n", + "operationId": "generatePdf", + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/GenerateMarkdownPdfRequest" + }, + { + "$ref": "#/components/schemas/GenerateHtmlPdfRequest" + } + ] + } + }, + "multipart/form-data": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/GenerateMarkdownPdfRequest" + }, + { + "$ref": "#/components/schemas/GenerateHtmlPdfRequest" + } + ] + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/pdf": { + "schema": { + "format": "binary", + "type": "string" + } + } + }, + "description": "The generated PDF document." + }, + "400": { + "description": "Validation error \u2014 invalid or missing fields in the request body." + }, + "401": { + "description": "Authentication required \u2014 API key or OAuth token is missing or invalid." + }, + "415": { + "description": "Unsupported media type \u2014 Content-Type must be application/json or multipart/form-data." + }, + "429": { + "description": "Concurrency limit exceeded \u2014 too many simultaneous requests on a development API key." + } + }, + "security": [ + { + "basicAuth": [] + }, + { + "oauth2": [] + } + ], + "summary": "Generate a PDF from HTML or Markdown", + "tags": [ + "PDF" + ] + } + } + }, + "servers": [ + { + "description": "Production", + "url": "https://app.useanvil.com" + } + ], + "tags": [] +} diff --git a/tests/test_api.py b/tests/test_api.py index d4fd0990..b965d3bd 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -133,6 +133,7 @@ def it_posts_a_dict_payload(anvil, rest): json={"data": {"jsonData": "yes"}}, params=None, data_type="bytes", + timeout=None, ) def it_posts_a_json_payload(anvil, rest): @@ -198,6 +199,7 @@ def it_posts_a_dict_payload_as_markdown_by_default(anvil, rest): json={"data": [{"d1": "data"}], "type": "markdown"}, params=None, data_type="bytes", + timeout=None, ) def it_posts_a_json_payload(anvil, rest): @@ -239,7 +241,7 @@ def describe_download_documents(): def it_gets_the_zip(anvil, rest): anvil.download_documents("dg123") rest.assert_called_once_with( - "GET", "/api/document-group/dg123.zip", data_type="bytes" + "GET", "/api/document-group/dg123.zip", data_type="bytes", timeout=None ) @@ -275,17 +277,19 @@ def it_raises_for_an_unexpected_body(anvil): def it_is_also_called_query(anvil, graphql): anvil.query("{ x }", {"a": 1}) - graphql.assert_called_once_with("{ x }", {"a": 1}) + graphql.assert_called_once_with("{ x }", {"a": 1}, timeout=None) def it_is_also_called_mutate(anvil, graphql): anvil.mutate("mutation { x }", {"a": 1}) - graphql.assert_called_once_with("mutation { x }", {"a": 1}) + graphql.assert_called_once_with("mutation { x }", {"a": 1}, timeout=None) def it_mutates_with_a_mutation_class(anvil, graphql): builder = CreateEtchPacket(name="Packet") anvil.mutate(builder, {"name": "Packet"}) graphql.assert_called_once_with( - mutations.create_etch_packet.generate_mutation(), {"name": "Packet"} + mutations.create_etch_packet.generate_mutation(), + {"name": "Packet"}, + timeout=None, ) @@ -385,6 +389,20 @@ def it_sends_a_payload_model_with_extra_fields(anvil, graphql): assert variables == cep.model_dump(by_alias=True, exclude_none=True) assert variables["newFeature"] is True + def it_takes_camel_case_keys_in_a_dict(anvil, graphql): + anvil.create_etch_packet( + { + "name": "Packet name", + "signatureEmailSubject": "The subject", + "webhookURL": "https://example.com/hook", + "mergePDFs": True, + } + ) + variables = graphql.call_args.args[1] + assert variables["signatureEmailSubject"] == "The subject" + assert variables["webhookURL"] == "https://example.com/hook" + assert variables["mergePDFs"] is True + def it_sends_a_custom_response_query(anvil, graphql): anvil.create_etch_packet( {"name": "Packet name"}, response_query="{ eid status }" @@ -419,7 +437,9 @@ def it_queries_the_packet(anvil, graphql): assert packet == {"eid": "p1", "status": "sent"} graphql.assert_called_once_with( - queries.etch_packet.generate_query(), {"eid": "p1"} + queries.etch_packet.generate_query(), + {"eid": "p1"}, + timeout=None, ) def it_sends_a_custom_response_query(anvil, graphql): @@ -444,6 +464,7 @@ def it_returns_the_url(anvil, graphql): graphql.assert_called_once_with( mutations.generate_etch_sign_url.generate_mutation(), {"signerEid": "someId", "clientUserId": "anotherId"}, + timeout=None, ) def it_is_also_called_generate_etch_sign_url(anvil, graphql): @@ -491,6 +512,16 @@ def it_sends_json(anvil, graphql): ) assert graphql.call_args.args[1] == expected_data + def it_takes_camel_case_keys_in_a_dict(anvil, graphql): + anvil.forge_submit( + { + "forgeEid": "forge1234", + "payload": {"field1": "Some data"}, + "weldDataEid": "wd1", + } + ) + assert graphql.call_args.args[1] == {**expected_data, "weldDataEid": "wd1"} + def it_sends_an_update_to_a_submission(anvil, graphql): payload = ForgeSubmitPayload( forge_eid="forge1234", @@ -526,7 +557,9 @@ def it_sends_the_mutation(anvil, graphql): assert anvil.remove_weld_data("wd1") is True graphql.assert_called_once_with( - mutations.remove_weld_data.generate_mutation(), {"eid": "wd1"} + mutations.remove_weld_data.generate_mutation(), + {"eid": "wd1"}, + timeout=None, ) def it_sends_a_custom_mutation(anvil, graphql): diff --git a/tests/test_compat.py b/tests/test_compat.py new file mode 100644 index 00000000..9d8f36e2 --- /dev/null +++ b/tests/test_compat.py @@ -0,0 +1,280 @@ +"""Call patterns from 5.x that must keep working, and the conveniences added for 6.0.""" + +from __future__ import annotations + +import io +import json +from pathlib import Path +from unittest import mock + +import pytest + +from python_anvil import Anvil +from python_anvil.api_resources.mutations import CreateEtchPacket, ForgeSubmit +from python_anvil.api_resources.mutations.base import BaseQuery +from python_anvil.api_resources.mutations.create_etch_packet import CREATE_ETCH_PACKET +from python_anvil.api_resources.mutations.forge_submit import FORGE_SUBMIT +from python_anvil.api_resources.payload import ( + DocumentUpload, + EtchCastRef, + EtchSigner, + GeneratePDFPayload, +) +from python_anvil.graphql import mutations, queries + + +DUMMY_PDF = Path(__file__).parent / "assets" / "dummy.pdf" + + +@pytest.fixture +def anvil(): + return Anvil(api_key="key") + + +@pytest.fixture +def graphql(anvil): + with mock.patch.object(anvil, "request_graphql", return_value={}) as m: + yield m + + +@pytest.fixture +def rest(anvil): + with mock.patch.object(anvil, "request_rest", return_value=b"%PDF-") as m: + yield m + + +def describe_the_constructor(): + def it_reads_the_api_key_from_the_environment(monkeypatch): + monkeypatch.setenv("ANVIL_API_KEY", "from-env") + client = Anvil() + assert client.client.api_key == "from-env" + + def it_still_requires_a_key_without_the_variable(monkeypatch): + monkeypatch.delenv("ANVIL_API_KEY", raising=False) + with pytest.raises(ValueError, match="or set ANVIL_API_KEY"): + Anvil() + + def it_keeps_the_credentials_on_the_client(anvil): + assert anvil.client.api_key == "key" + assert anvil.client.access_token is None + + +def describe_query_and_mutate(): + def they_take_a_timeout(anvil, graphql): + anvil.query("{ x }", timeout=10) + assert graphql.call_args.kwargs["timeout"] == 10 + anvil.mutate("mutation { x }", {"a": 1}, timeout=(5, 30)) + assert graphql.call_args.kwargs["timeout"] == (5, 30) + + def they_take_variables_as_a_json_string(anvil): + response = mock.Mock( + status_code=200, headers={"content-type": "application/json"} + ) + response.json.return_value = {"data": {"x": 1}} + with mock.patch.object( + anvil.client, "request", return_value=response + ) as request: + anvil.query("query ($eid: String) { x(eid: $eid) }", '{"eid": "w1"}') + assert request.call_args.kwargs["json"]["variables"] == {"eid": "w1"} + + def a_5x_subclass_with_a_mutation_template_still_works(anvil, graphql): + class Mine(BaseQuery): + mutation = "mutation Mine($eid: String!) {{ mine(eid: $eid) {query} }}" + mutation_res_query = "{ eid }" + + def create_payload(self): + return {"eid": "x"} + + anvil.mutate(Mine(), {"eid": "x"}) + assert ( + graphql.call_args.args[0] + == "mutation Mine($eid: String!) { mine(eid: $eid) { eid } }" + ) + + def the_old_module_constants_are_the_mutations(): + assert mutations.create_etch_packet.generate_mutation() == CREATE_ETCH_PACKET + assert mutations.forge_submit.generate_mutation() == FORGE_SUBMIT + + +def describe_every_method_taking_a_timeout(): + def rest_methods(anvil, rest): + anvil.fill_pdf("t", {"data": {"a": 1}}, timeout=7) + assert rest.call_args.kwargs["timeout"] == 7 + anvil.generate_pdf({"data": [{"content": "x"}]}, timeout=8) + assert rest.call_args.kwargs["timeout"] == 8 + anvil.download_documents("dg", timeout=9) + assert rest.call_args.kwargs["timeout"] == 9 + + def graphql_methods(anvil, graphql): + graphql.return_value = { + "createEtchPacket": {}, + "etchPacket": {}, + "generateEtchSignURL": "u", + "forgeSubmit": {}, + "removeWeldData": True, + } + anvil.create_etch_packet({"name": "n"}, timeout=1) + assert graphql.call_args.kwargs["timeout"] == 1 + anvil.get_etch_packet("e", timeout=2) + assert graphql.call_args.kwargs["timeout"] == 2 + anvil.generate_etch_signing_url("s", "c", timeout=3) + assert graphql.call_args.kwargs["timeout"] == 3 + anvil.forge_submit({"forge_eid": "f", "payload": {}}, timeout=4) + assert graphql.call_args.kwargs["timeout"] == 4 + anvil.remove_weld_data("w", timeout=5) + assert graphql.call_args.kwargs["timeout"] == 5 + + +def describe_create_etch_packet_payloads(): + def an_empty_dict_is_a_type_error_as_before(anvil, graphql): + with pytest.raises(TypeError, match="`payload` or `json` must exist"): + anvil.create_etch_packet({}) + with pytest.raises(TypeError, match="`payload` or `json` must exist"): + anvil.create_etch_packet(payload={}) + with pytest.raises(TypeError, match="`name` is required"): + CreateEtchPacket() + + def a_json_string_works_as_the_payload(anvil, graphql): + anvil.create_etch_packet(json.dumps({"name": "n", "signers": [], "files": []})) + assert graphql.call_args.args[1]["name"] == "n" + + def a_dict_may_hold_every_kind_of_file_and_model_instances(anvil, graphql): + anvil.create_etch_packet( + { + "name": "n", + "files": [ + {"id": "tpl", "castEid": "cast1"}, + {"id": "md", "filename": "md.pdf", "fields": [{"content": "hi"}]}, + {"id": "html", "filename": "h.pdf", "markup": {"html": "

"}}, + { + "id": "up", + "title": "T", + "file": { + "data": "aGk=", + "filename": "x.pdf", + "mimetype": "application/pdf", + }, + "fields": [], + }, + EtchCastRef(id="tpl2", cast_eid="cast2"), + ], + "signers": [EtchSigner(name="S", email="s@example.com", fields=[])], + } + ) + files = graphql.call_args.args[1]["files"] + assert [f["id"] for f in files] == ["tpl", "md", "html", "up", "tpl2"] + assert files[0] == {"id": "tpl", "castEid": "cast1"} + assert files[1]["fields"][0]["content"] == "hi" + assert files[2]["markup"] == {"html": "

"} + assert files[3]["file"]["data"] == "aGk=" + assert graphql.call_args.args[1]["signers"][0]["name"] == "S" + + def an_in_person_signer_is_accepted(anvil, graphql): + packet = CreateEtchPacket(name="n") + packet.add_signer( + EtchSigner( + name="S", email="s@example.com", fields=[], signer_type="in-person" + ) + ) + anvil.create_etch_packet(packet) + assert graphql.call_args.args[1]["signers"][0]["signerType"] == "in-person" + + def response_fields_may_be_given_without_braces(anvil, graphql): + graphql.return_value = {"createEtchPacket": {}, "etchPacket": {}} + anvil.create_etch_packet({"name": "n"}, response_query="eid name") + assert graphql.call_args.args[ + 0 + ] == mutations.create_etch_packet.generate_mutation("{ eid name }") + anvil.get_etch_packet("e", response_query="eid") + assert graphql.call_args.args[0] == queries.etch_packet.generate_query( + "{ eid }" + ) + + def an_uploaded_file_keeps_its_own_name(): + with DUMMY_PDF.open("rb") as pdf: + upload = DocumentUpload(id="f", title="T", file=pdf, fields=[]) + dumped = upload.model_dump(by_alias=True) + assert dumped["file"]["filename"] == "dummy.pdf" + assert dumped["file"]["mimetype"] == "application/pdf" + + with DUMMY_PDF.open("rb") as pdf: + packet = CreateEtchPacket(name="n") + packet.add_file(DocumentUpload(id="f", title="T", file=pdf, fields=[])) + dumped = packet.create_payload().model_dump(by_alias=True) + assert dumped["files"][0]["file"]["filename"] == "dummy.pdf" + + def a_file_with_no_name_is_still_document_pdf(): + dumped = DocumentUpload( + id="f", title="T", file=io.BytesIO(b"x"), fields=[] + ).model_dump(by_alias=True) + assert dumped["file"]["filename"] == "document.pdf" + + +def describe_forge_submit_payloads(): + def an_empty_dict_is_a_type_error_as_before(anvil, graphql): + with pytest.raises(TypeError, match="`json` or `payload` are required"): + anvil.forge_submit({}) + + def a_json_string_works_as_the_payload(anvil, graphql): + anvil.forge_submit(json.dumps({"forgeEid": "f", "payload": {"a": 1}})) + assert graphql.call_args.args[1]["forgeEid"] == "f" + + def the_builder_still_takes_a_dict(anvil, graphql): + anvil.mutate( + ForgeSubmit({"a": 1}, forge_eid="f"), {"forgeEid": "f", "payload": {"a": 1}} + ) + assert "forgeSubmit" in graphql.call_args.args[0] + + +def describe_generate_pdf_payload(): + def it_dumps_the_documented_examples_as_before(): + markdown = GeneratePDFPayload( + title="Example", + data=[ + {"label": "Name", "content": "Sally"}, + {"table": {"firstRowHeaders": True, "rows": [["a"]]}}, + ], + ) + assert markdown.model_dump(by_alias=True, exclude_none=True) == { + "title": "Example", + "type": "markdown", + "data": [ + {"label": "Name", "content": "Sally"}, + {"table": {"firstRowHeaders": True, "rows": [["a"]]}}, + ], + } + html = GeneratePDFPayload( + type="html", title="T", data={"html": "

h

", "css": "h2 {}"} + ) + assert html.model_dump(by_alias=True, exclude_none=True) == { + "title": "T", + "type": "html", + "data": {"html": "

h

", "css": "h2 {}"}, + } + + def it_takes_every_documented_option(): + payload = GeneratePDFPayload( + data=[{"content": "x"}], + logo={"src": "https://x/l.png", "max_width": 100}, + page={"marginTop": "1in", "pageCount": "topRight"}, + font_size=36, + font_family="Helvetica", + text_color="#333", + include_timestamp=False, + ) + dumped = payload.model_dump(by_alias=True, exclude_none=True) + assert dumped["logo"] == {"src": "https://x/l.png", "maxWidth": 100} + assert dumped["page"] == {"marginTop": "1in", "pageCount": "topRight"} + # A documented page option is validated; an unknown one is kept + assert GeneratePDFPayload( + data=[{"content": "x"}], page={"customKey": 1} + ).model_dump(by_alias=True, exclude_none=True)["page"] == {"customKey": 1} + assert dumped["fontSize"] == 36 + assert dumped["includeTimestamp"] is False + assert "outputFormat" not in dumped + + def it_rejects_what_the_api_rejects(): + with pytest.raises(ValueError): + GeneratePDFPayload(data=[{"content": "x"}], page={"pageCount": "middle"}) + with pytest.raises(ValueError): + GeneratePDFPayload(data=[{"content": "x"}], type="docx") diff --git a/tests/test_generate_types.py b/tests/test_generate_types.py new file mode 100644 index 00000000..afaa2594 --- /dev/null +++ b/tests/test_generate_types.py @@ -0,0 +1,47 @@ +"""The parts of dev/generate_types.py that post-process the generator's output.""" + +from __future__ import annotations + +import importlib.util +import sys +from pathlib import Path + + +def load_generator(): + path = Path(__file__).parent.parent / "dev" / "generate_types.py" + spec = importlib.util.spec_from_file_location("generate_types", path) + assert spec and spec.loader + module = importlib.util.module_from_spec(spec) + sys.modules["generate_types"] = module + spec.loader.exec_module(module) + return module + + +def describe_use_plain_str(): + def it_rewrites_a_one_line_import(): + gen = load_generator() + source = "from pydantic import AwareDatetime, ConfigDict, EmailStr, Field\n\nemail: EmailStr | None\nwhen: AwareDatetime\n" + out = gen._use_plain_str(source, ["EmailStr", "AwareDatetime"]) + assert ( + out + == "from pydantic import ConfigDict, Field\n\nemail: str | None\nwhen: str\n" + ) + + def it_rewrites_a_wrapped_import(): + gen = load_generator() + source = ( + "from pydantic import (\n AnyUrl,\n AwareDatetime,\n ConfigDict,\n" + " EmailStr,\n Field,\n)\n\nemail: EmailStr\n" + ) + out = gen._use_plain_str(source, ["EmailStr", "AwareDatetime"]) + assert ( + out + == "from pydantic import (\n AnyUrl,\n ConfigDict,\n Field,\n)\n\nemail: str\n" + ) + assert "import" in out and " str," not in out + + def it_keeps_acronym_plurals_whole_in_snake_case(): + gen = load_generator() + assert gen._snake_case("mergePDFs") == "merge_pdfs" + assert gen._snake_case("webhookURL") == "webhook_url" + assert gen._snake_case("signURLForUser") == "sign_url_for_user" diff --git a/tests/test_types.py b/tests/test_types.py new file mode 100644 index 00000000..02b030b5 --- /dev/null +++ b/tests/test_types.py @@ -0,0 +1,233 @@ +"""The generated types, and the payload models built on them.""" + +from __future__ import annotations + +import json +import re +from pathlib import Path + +import pytest +from pydantic import ValidationError + +from python_anvil import types +from python_anvil.api_resources.payload import ( + CreateEtchPacketPayload, + EtchSigner, + FillPDFPayload, + ForgeSubmitPayload, +) +from python_anvil.graphql import mutations, queries +from python_anvil.types import GeneratedModel, graphql, operations, rest + + +def describe_the_generated_models(): + def they_take_snake_case_and_camel_case_names(): + by_snake = rest.FillPdfRequest( + data={"a": 1}, font_size=10, use_interactive_fields=True + ) + by_camel = rest.FillPdfRequest( + data={"a": 1}, fontSize=10, useInteractiveFields=True + ) + assert by_snake == by_camel + assert by_snake.font_size == 10 + + def they_dump_camel_case_json(): + request = rest.FillPdfRequest( + data={"a": 1}, text_color="#333", use_interactive_fields=True + ) + assert request.model_dump(by_alias=True, exclude_none=True) == { + "data": {"a": 1}, + "textColor": "#333", + "useInteractiveFields": True, + "outputFormat": "pdf", + } + + def they_keep_extra_fields(): + request = rest.PageOptions(width="8.5in", brandNew=True) # type: ignore[call-arg] + assert request.model_dump(by_alias=True, exclude_none=True) == { + "width": "8.5in", + "brandNew": True, + } + + def they_check_enum_values(): + assert rest.EtchSigner(id="s", signature_mode="draw").signature_mode == "draw" + with pytest.raises(ValidationError): + rest.EtchSigner(id="s", signature_mode="wave") + + def they_are_all_generated_models(): + for model in ( + rest.FillPdfRequest, + graphql.EtchFile, + graphql.EtchPacket, + operations.CreateEtchPacketArgs, + ): + assert issubclass(model, GeneratedModel) + + def the_package_exports_the_useful_ones(): + assert types.FillPdfRequest is rest.FillPdfRequest + assert types.CreateEtchPacketArgs is operations.CreateEtchPacketArgs + assert types.EtchPacketArgs is operations.EtchPacketArgs + + +def describe_the_operation_argument_models(): + def they_have_the_schema_argument_names_as_aliases(): + args = operations.CreateEtchPacketArgs( + name="x", webhook_url="https://example.com", merge_pdfs=True + ) + assert args.model_dump(by_alias=True, exclude_none=True) == { + "name": "x", + "webhookURL": "https://example.com", + "mergePDFs": True, + } + + def they_require_the_required_arguments(): + with pytest.raises(ValidationError): + operations.ForgeSubmitArgs(payload={}) # type: ignore[call-arg] + assert operations.ForgeSubmitArgs(forge_eid="f", payload={}).forge_eid == "f" + + def they_type_input_objects_from_the_schema(): + args = operations.CreateEtchPacketArgs(files=[{"id": "f1", "castEid": "cast1"}]) + assert args.files is not None + assert isinstance(args.files[0], graphql.EtchFile) + assert args.model_dump(by_alias=True, exclude_none=True)["files"] == [ + {"id": "f1", "castEid": "cast1"} + ] + + def they_rename_arguments_that_pydantic_uses(): + assert "schema_" in operations.CreateCastArgs.model_fields + assert operations.CreateCastArgs.model_fields["schema_"].alias == "schema" + + @pytest.mark.parametrize( + ("operation", "model"), + [ + ( + mutations.create_etch_packet.generate_mutation(), + operations.CreateEtchPacketArgs, + ), + (mutations.forge_submit.generate_mutation(), operations.ForgeSubmitArgs), + ( + mutations.generate_etch_sign_url.generate_mutation(), + operations.GenerateEtchSignURLArgs, + ), + ( + mutations.remove_weld_data.generate_mutation(), + operations.RemoveWeldDataArgs, + ), + (queries.etch_packet.generate_query(), operations.EtchPacketArgs), + ], + ids=[ + "createEtchPacket", + "forgeSubmit", + "generateEtchSignURL", + "removeWeldData", + "etchPacket", + ], + ) + def the_client_operations_only_use_arguments_the_model_has(operation, model): + variables = set(re.findall(r"\$(\w+):", operation)) + aliases = {field.alias or name for name, field in model.model_fields.items()} + assert variables <= aliases + + +def describe_the_payload_models(): + def fill_pdf_payload_takes_every_fill_option(): + payload = FillPDFPayload( + data={"a": 1}, font_size=10, use_interactive_fields=True, borderColor="#000" + ) + assert isinstance(payload, rest.FillPdfRequest) + assert payload.model_dump(by_alias=True, exclude_none=True) == { + "data": {"a": 1}, + "fontSize": 10, + "useInteractiveFields": True, + "borderColor": "#000", + } + + def fill_pdf_payload_still_rejects_empty_data(): + with pytest.raises(ValidationError): + FillPDFPayload(data={}) + + def etch_signer_takes_every_signer_option(): + signer = EtchSigner( + name="Sally", + email="sally@example.com", + fields=[{"file_id": "f", "field_id": "sig"}], + signer_type="embedded", + signature_mode="draw", + allow_name_change=True, + redirect_url="https://example.com/done", + ) + assert isinstance(signer, rest.EtchSigner) + assert signer.model_dump(by_alias=True, exclude_none=True) == { + "name": "Sally", + "email": "sally@example.com", + "fields": [{"fileId": "f", "fieldId": "sig"}], + "signerType": "embedded", + "signatureMode": "draw", + "allowNameChange": True, + "redirectURL": "https://example.com/done", + } + + def etch_signer_passes_dates_through_as_strings(): + # A datetime object would not survive the JSON encoder, and a date with + # no timezone must still be the API's call + for value in ("2030-01-01T00:00:00Z", "2030-01-01T00:00:00"): + signer = EtchSigner( + name="S", email="s@example.com", fields=[], tokenValidUntil=value + ) + dumped = signer.model_dump(by_alias=True, exclude_none=True) + assert dumped["tokenValidUntil"] == value + json.dumps(dumped) + + def payloads_keep_accepting_any_font_size_and_routing_order(): + assert ( + FillPDFPayload(data={"a": 1}, font_size=36).model_dump(by_alias=True)[ + "fontSize" + ] + == 36 + ) + signer = EtchSigner(name="S", email="s@example.com", fields=[], routing_order=0) + assert signer.model_dump(by_alias=True, exclude_none=True)["routingOrder"] == 0 + + def etch_signer_rejects_an_unknown_signer_type(): + with pytest.raises(ValidationError): + EtchSigner( + name="S", email="s@example.com", fields=[], signer_type="carrier pigeon" + ) + + def create_etch_packet_payload_is_the_mutation_arguments(): + payload = CreateEtchPacketPayload( + name="Packet", + signers=[], + files=[], + allow_updates=True, + advanced_create=True, + ) + assert isinstance(payload, operations.CreateEtchPacketArgs) + assert payload.model_dump(by_alias=True, exclude_none=True) == { + "name": "Packet", + "signers": [], + "files": [], + "isDraft": False, + "isTest": True, + "allowUpdates": True, + "advancedCreate": True, + } + + def forge_submit_payload_is_the_mutation_arguments(): + payload = ForgeSubmitPayload( + forge_eid="f", payload={"a": 1}, group_array_id="g" + ) + assert isinstance(payload, operations.ForgeSubmitArgs) + assert payload.model_dump(by_alias=True, exclude_none=True) == { + "forgeEid": "f", + "payload": {"a": 1}, + "isTest": True, + "groupArrayId": "g", + } + + +def describe_the_generated_files(): + def they_say_they_are_generated(): + for name in ("rest.py", "graphql.py", "operations.py"): + source = (Path(types.__file__).parent / name).read_text() + assert source.startswith("# Generated by dev/generate_types.py") diff --git a/tests/test_utils.py b/tests/test_utils.py index 40be674a..f61c3885 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -35,3 +35,7 @@ def test_it(): assert camel_to_snake("oneToTwo") == "one_to_two" assert camel_to_snake("one") == "one" assert camel_to_snake("TwoTwo") == "two_two" + assert camel_to_snake("webhookURL") == "webhook_url" + assert camel_to_snake("mergePDFs") == "merge_pdfs" + assert camel_to_snake("signURLForUser") == "sign_url_for_user" + assert camel_to_snake("already_snake") == "already_snake"