Latest Threat Research:SANDWORM_MODE: Shai-Hulud-Style npm Worm Hijacks CI Workflows and Poisons AI Toolchains.Details
Socket
Book a DemoInstallSign in
Socket

erdantic

Package Overview
Dependencies
Maintainers
1
Versions
27
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

erdantic - npm Package Compare versions

Comparing version
1.0.5
to
1.1.0
+2
.gitattributes
# SCM syntax highlighting & preventing 3-way merges
pixi.lock merge=binary linguist-language=YAML linguist-generated=true
name: docs-preview
on:
pull_request:
workflow_dispatch:
jobs:
build:
name: Build docs and deploy preview
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: extractions/setup-just@v2
- uses: prefix-dev/setup-pixi@v0.8.5
with:
cache: true
cache-key: default
environments: default
- name: Build docs
run: |
just docs
- name: Get preview identifier
id: get_id
run: |
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
PREVIEW_IDENTIFIER="PR${{ github.event.number }}"
else
PREVIEW_IDENTIFIER="$(git rev-parse --short HEAD)"
fi
echo "PREVIEW_IDENTIFIER=$PREVIEW_IDENTIFIER" | tee -a $GITHUB_OUTPUT
- name: Deploy preview to Netlify
uses: nwtgck/actions-netlify@v2.0.0
with:
publish-dir: "./docs/site"
production-deploy: false
github-token: ${{ secrets.GITHUB_TOKEN }}
deploy-message: "Deploy from GitHub Actions"
enable-pull-request-comment: true
enable-commit-comment: false
overwrites-pull-request-comment: true
alias: ${{ steps.get_id.outputs.PREVIEW_IDENTIFIER }}
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
timeout-minutes: 1
name: docs-publish
on:
push:
branches: [main]
workflow_dispatch:
inputs:
deploy-as:
type: choice
description: Deploy as
options:
- latest
- version
default: latest
is-stable:
type: boolean
description: Deploy as stable
default: false
version-format:
type: string
description: Version format
default: "v{major_minor_version}"
jobs:
build:
name: Stage docs on gh-pages
runs-on: ubuntu-latest
steps:
- name: Fail if deploying as latest but marked as stable
if: github.event.inputs.deploy-as == 'latest' && github.event.inputs.is-stable == 'true'
run: |
echo "Error: Cannot deploy as 'latest' when 'is-stable' is true."
exit 1
- uses: actions/checkout@v4
- uses: extractions/setup-just@v2
- uses: prefix-dev/setup-pixi@v0.8.5
with:
cache: true
cache-key: default
environments: default
- name: Set git user to github-actions[bot]
run: |
git config user.name github-actions[bot]
git config user.email 41898282+github-actions[bot]@users.noreply.github.com
- name: Fetch gh-pages branch
run: |
git fetch origin gh-pages --depth=1
- name: Deploy as latest
if: github.event.inputs.deploy-as == 'latest' || github.event_name == 'push'
working-directory: docs/
run: |
pixi run mike deploy --push latest --title=latest
- name: Get version tag
id: get-version-tag
if: github.event.inputs.deploy-as == 'version'
run: |
VERSION_TAG=$(pixi run vspect read . "${{ github.event.inputs.version-format }}")
echo "VERSION_TAG=${VERSION_TAG}" | tee -a $GITHUB_OUTPUT
- name: Deploy as version
if: github.event.inputs.deploy-as == 'version'
working-directory: docs/
run: |
TITLE=$(uv run vspect read .. "v{version}")
TAG=${{ steps.get-version-tag.outputs.VERSION_TAG }}
pixi run mike deploy --push $TAG --title="${TITLE}"
- name: Assign stable alias
if: github.event.inputs.is-stable == 'true'
working-directory: docs/
run: |
TAG=${{ steps.get-version-tag.outputs.VERSION_TAG }}
pixi run mike alias --push --update-aliases $TAG stable
deploy:
name: Deploy docs to Netlify
needs: build
runs-on: "ubuntu-latest"
steps:
- uses: actions/checkout@v4
with:
ref: gh-pages
- name: Deploy docs to Netlify
uses: nwtgck/actions-netlify@v1.1
with:
publish-dir: "./"
production-deploy: true
github-token: ${{ secrets.GITHUB_TOKEN }}
deploy-message: "Deploy from GitHub Actions"
enable-pull-request-comment: false
enable-commit-comment: false
overwrites-pull-request-comment: false
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
timeout-minutes: 1
# erdantic Changelog
## v1.1.0 (2025-04-12)
- Removed support for Python 3.8.
- Added Python 3.13 to supported Python versions.
- Added support for [msgspec structs](https://jcristharif.com/msgspec/structs.html). ([Issue #139](https://github.com/drivendataorg/erdantic/issues/139), [PR #142](https://github.com/drivendataorg/erdantic/pull/142))
- Added `--list-plugins` CLI flag to print information on active plugins. ([PR #144](https://github.com/drivendataorg/erdantic/pull/144))
## v1.0.5 (2024-09-19)
- Fixed runtime `AttributeError` that occurred when creating a diagram that includes a model with a field that uses a type annotation with the ellipsis literal (e.g., `tuple[int, ...]`). ([Issue #124](https://github.com/drivendataorg/erdantic/issues/124), [PR #127](https://github.com/drivendataorg/erdantic/pull/127))
## v1.0.4 (2024-07-16)
- Fixed handling of `typing.Annotated` in cases where it's not the outermost generic type. ([Issue #122](https://github.com/drivendataorg/erdantic/issues/122), [PR #123](https://github.com/drivendataorg/erdantic/pull/123))
## v1.0.3 (2024-05-10)
- Fixed `StopIteration` error when rendering a model that has no fields. ([Issue #120](https://github.com/drivendataorg/erdantic/issues/120), [PR #121](https://github.com/drivendataorg/erdantic/pull/121))
## v1.0.2 (2024-04-11)
- Fixed `AttributeError` when adding a model that has a field annotated with certain typing special forms like `Any`, `Literal`, or `TypeVar` instances. ([Issue #114](https://github.com/drivendataorg/erdantic/issues/114), [PR #115](https://github.com/drivendataorg/erdantic/pull/115))
## v1.0.1 (2024-04-10)
- Fixed `ModuleNotFoundError` when importing from `erdantic.examples` without attrs installed.
## v1.0.0.post2 (2024-04-10)
- Fixed missing LICENSE file in sdist.
## v1.0.0.post1 (2024-04-09)
- Fixed outdated note in README.
## v1.0.0 (2024-04-09)
> [!IMPORTANT]
> This release features significant changes to erdantic, primarily to the backend process of analyzing models and representing data. If you have been primarily using the CLI or the convenience functions `create`, `draw`, and `to_dot`, then your code may continue to work without any changes. If you are doing something more advanced, you may need to update your code.
### CLI changes
- Deprecated `--termini` option. Use the new `--terminal-model` option instead. The shorthand option `-t` remains the same. The `--termini` option still works but will emit a deprecation warning.
### Convenience function changes
- Deprecated `termini` argument for `create`, `draw`, and `to_dot` functions. Use the new `terminal_models` argument instead. The `termini` argument still works but will emit a deprecation warning.
- Added `graph_attr`, `node_attr`, and `edge_attr` arguments to the `draw` and `to_dot` functions that allow you to override attributes on the generated pygraphviz object for the diagram.
### Visual changes
A few changes have been made to the visual content of rendered diagrams.
- Changed the extraction of type names to use the [typenames](https://github.com/jayqi/typenames) library. This should generally produce identical rendered outputs as before, with the following exception:
- Removed the special case behavior for rendering enum classes. Enums now just show the class name without inheritance information.
- Changed collection fields (e.g., `List[TargetModel]`) to display as a "many" relationship (crow) instead of a "zero-or-many" relationship (odot + crow), treating the modality of the field as unspecified. A field will only be displayed as "zero-or-many" (odot + crow) if it is explicitly optional, like `Optional[List[TargetModel]]`.
- Fixed incorrect representation of manyness for type annotations where the outermost annotation wasn't a collection type. ([Issue #105](https://github.com/drivendataorg/erdantic/issues/105))
### Support for attrs
- Added support for [attrs](https://www.attrs.org/en/stable/index.html) classes, i.e., classes decorated by `attrs.define`. The source code for attrs support can be found in the new module `erdantic.plugins.attrs`.
- Added new example module `erdantic.examples.attrs`.
### Backend changes
Significant changes have been made to the library backend to more strongly separate the model analysis process, the extracted data, and the diagram rendering process. We believe this more structured design facilitates customizing diagrams and simplifies the implementation for each data modeling framework. Please see the new documentation pages ["Customizing diagrams"](http://erdantic.drivendata.org/v1.0/customizing/) and ["Extending or modifying erdantic"](http://erdantic.drivendata.org/v1.0/extending/) for details on the new design.
A summary of some key changes is below:
- Removed the adapter base classes `Model` and `Field` and the conrete adapters `DataClassModel`, `DataClassField`, `PydanticModel`, and `PydanticField`.
- Added new Pydantic models `ModelInfo` and `FieldInfo` to replace the adapter system. These new models hold static data that have been extracted from models that erdantic analyzed.
- Removed the adapter system and associated objects such as `model_adapter_registry` and `register_model_adapter`.
- Added new plugin system to replace the adapter system as the way that modeling frameworks are supported. Plugins must implement two functions—a predicate function and a field extractor function—and be registered using `register_plugin`. All objects related to plugins can be found in the new `erdantic.plugins` module and its submodules.
- Renamed `erdantic.typing` module to `erdantic.typing_utils`.
### Other
- Added [PEP 561 `py.typed` marker file](https://peps.python.org/pep-0561/#packaging-type-information) to indicate that the package supports type checking.
- Added IPython special method for pretty-print string representations of `EntityRelationshipDiagram` instances.
- Removed support for Python 3.7. ([PR #102](https://github.com/drivendataorg/erdantic/pull/102))
## v0.7.1 (2024-04-09)
This will be the last version that supports Python 3.7.
- Added version typer version ceiling of `< 0.10.0` due to incompatibility with a fix introduced in that version.
## v0.7.0 (2024-02-11)
- Added support for Pydantic V1 legacy models. These are models created from the `pydantic.v1` namespace when Pydantic V2 is installed. ([PR #94](https://github.com/drivendataorg/erdantic/pull/94) from [@ursereg](https://github.com/ursereg))
## v0.6.0 (2023-07-09)
- Added support for Pydantic V2.
- Removed support for Pydantic V1.
- Changed the init signature for `PydanticField` to work with Pydantic V2's API.
- Added `is_many` and `is_nullable` functions to `erdantic.typing`.
## v0.5.1 (2023-07-04)
- Changed Pydantic dependency to be `< 2`. This will be the final version of erdantic that supports Pydantic V1.
- Changed to pyproject.toml-based build.
## v0.5.0 (2022-07-29)
- Removed support for Python 3.6. ([Issue #51](https://github.com/drivendataorg/erdantic/issues/51), [PR #56](https://github.com/drivendataorg/erdantic/pull/56))
- Added support for modules as inputs to all entrypoints to diagram creation (`create`, `draw`, `to_dot`, CLI). For all modules passed, erdantic will find all supported data model classes in each module. ([Issue #23](https://github.com/drivendataorg/erdantic/issues/23), [PR #58](https://github.com/drivendataorg/erdantic/pull/58))
- Added new parameter `limit_search_models_to` to all entrypoints to allow for limiting which data model classes will be yielded from searching a module.
## v0.4.1 (2022-04-08)
- Fixed error when rendering a data model that has field using `typing.Literal`. ([PR #49](https://github.com/drivendataorg/erdantic/pull/49))
## v0.4.0 (2021-11-06)
- Added support for showing field documentation from Pydantic models with descriptions set with `Field(description=...)` in SVG tooltips. This will add an "Attributes" section to the tooltip using Google-style docstring format and lists fields where the `description` keyword argument is used. ([Issue #8](https://github.com/drivendataorg/erdantic/issues/8#issuecomment-958905131), [PR #42](https://github.com/drivendataorg/erdantic/pull/42))
## v0.3.0 (2021-10-28)
- Fixed handling of forward references in field type declarations. Evaluated forward references will be properly identified. Forward references not converted to `typing.ForwardRef` will throw a `StringForwardRefError` with instructions for how to resolve. Unevaluated forward references will throw an `UnevaluatedForwardRefError` with instructions for how to resolve. See [new documentation](https://erdantic.drivendata.org/stable/forward-references/) for more details. ([Issue #40](https://github.com/drivendataorg/erdantic/issues/40), [PR #41](https://github.com/drivendataorg/erdantic/pull/41))
- Changed name of `erdantic.errors` module to `erdantic.exceptions`. ([PR #41](https://github.com/drivendataorg/erdantic/issues/41))
- Added new `ErdanticException` base class from which other exceptions raised within the erdantic library are subclassed from. Changed several existing `ValueError` exceptions to new exception classes that subclass both `ErdanticException` and `ValueError`. ([PR #41](https://github.com/drivendataorg/erdantic/issues/41))
- Changed `__lt__` method on `Model` and `Edge` to return `NotImplemented` instead of raising an exception to follow typical convention for unsupported input types. ([PR #41](https://github.com/drivendataorg/erdantic/issues/41))
## v0.2.1 (2021-02-16)
- Fixed runtime error when rendering a data model that had a field containing `typing.Any`. ([Issue #25](https://github.com/drivendataorg/erdantic/issues/25), [PR #26](https://github.com/drivendataorg/erdantic/issues/26))
## v0.2.0 (2021-02-14)
- Added option to specify models as terminal nodes. This allows you to truncate large diagrams and split them up into smaller ones. ([PR #24](https://github.com/drivendataorg/erdantic/pull/24))
## v0.1.2 (2021-02-10)
- Fixed bug where Pydantic fields were missing generics in their type annotations. ([PR #19](https://github.com/drivendataorg/erdantic/pull/19))
- Added tests against static rendered DOT output. Change adapter tests to use parameterized fixtures. ([PR #21](https://github.com/drivendataorg/erdantic/pull/21))
## v0.1.1 (2021-02-10)
- Fixed rendered example image in the package description on PyPI. ([PR #18](https://github.com/drivendataorg/erdantic/pull/18))
## v0.1.0 (2021-02-10)
Initial release! 🎉
# erdantic.examples.msgspec
::: erdantic.examples.msgspec
# erdantic.plugins.msgspec
::: erdantic.plugins.msgspec

Sorry, the diff of this file is not supported yet

"""Example struct classes using [msgspec](https://jcristharif.com/msgspec/)."""
from datetime import datetime
from enum import Enum
from typing import List, Optional
import msgspec
class Alignment(str, Enum):
LAWFUL_GOOD = "lawful_good"
NEUTRAL_GOOD = "neutral_good"
CHAOTIC_GOOD = "chaotic_good"
LAWFUL_NEUTRAL = "lawful_neutral"
TRUE_NEUTRAL = "true_neutral"
CHAOTIC_NEUTRAL = "chaotic_neutral"
LAWFUL_EVIL = "lawful_evil"
NEUTRAL_EVIL = "neutral_evil"
CHAOTIC_EVIL = "chaotic_evil"
class Adventurer(msgspec.Struct):
"""A person often late for dinner but with a tale or two to tell.
Attributes:
name (str): Name of this adventurer
profession (str): Profession of this adventurer
alignment (Alignment): Alignment of this adventurer
level (int): Level of this adventurer
"""
name: str
profession: str
alignment: Alignment
level: int = 1
class QuestGiver(msgspec.Struct):
"""A person who offers a task that needs completing.
Attributes:
name (str): Name of this quest giver
faction (str): Faction that this quest giver belongs to
location (str): Location this quest giver can be found
"""
name: str
faction: Optional[str] = None
location: str = "Adventurer's Guild"
class Quest(msgspec.Struct):
"""A task to complete, with some monetary reward.
Attributes:
name (str): Name by which this quest is referred to
giver (QuestGiver): Person who offered the quest
reward_gold (int): Amount of gold to be rewarded for quest completion
"""
name: str
giver: QuestGiver
reward_gold: int = 100
class Party(msgspec.Struct):
"""A group of adventurers finding themselves doing and saying things altogether unexpected.
Attributes:
name (str): Name that party is known by
formed_datetime (datetime): Timestamp of when the party was formed
members (List[Adventurer]): Adventurers that belong to this party
active_quest (Optional[Quest]): Current quest that party is actively tackling
"""
name: str
formed_datetime: datetime
members: List[Adventurer] = []
active_quest: Optional[Quest] = None
import re
import sys
from typing import Any, List, Type
if sys.version_info >= (3, 10):
from typing import TypeGuard
else:
from typing_extensions import TypeGuard
import msgspec
from erdantic.core import FieldInfo, FullyQualifiedName
from erdantic.exceptions import UnresolvableForwardRefError
from erdantic.plugins import register_plugin
MsgspecStruct = Type[msgspec.Struct]
def is_msgspec_struct(obj: Any) -> TypeGuard[MsgspecStruct]:
"""Predicate function to determine if an object is a msgspect struct (class, not instance).
Args:
obj (Any): The object to check.
Returns:
bool: True if the object is a Struct class, False otherwise.
"""
return isinstance(obj, type) and issubclass(obj, msgspec.Struct)
def get_fields_from_msgspec_struct(model: MsgspecStruct) -> List[FieldInfo]:
"""Given a msgspec struct, return a list of FieldInfo instances for each field in the
struct.
Args:
model (PydanticModel): The struct to get fields from.
Returns:
List[FieldInfo]: List of FieldInfo instances for each field in the struct
"""
try:
msgspec._utils.get_class_annotations(model) # type: ignore [attr-defined]
return [
FieldInfo.from_raw_type(
model_full_name=FullyQualifiedName.from_object(model),
name=msgspec_field_info.name,
raw_type=msgspec_field_info.type,
)
for msgspec_field_info in msgspec.structs.fields(model)
]
except NameError as e:
model_full_name = FullyQualifiedName.from_object(model)
forward_ref = getattr(
e,
"name",
re.search(r"(?<=')(?:[^'])*(?=')", str(e)).group(0), # type: ignore [union-attr]
)
msg = (
f"Failed to resolve forward reference '{forward_ref}' in the type annotations for "
f"struct {model_full_name}. "
"erdantic does not currently support manually resolving forward references for "
"structs."
)
raise UnresolvableForwardRefError(
msg, name=forward_ref, model_full_name=model_full_name
) from e
register_plugin(
key="msgspec", predicate_fn=is_msgspec_struct, get_fields_fn=get_fields_from_msgspec_struct
)

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is too big to display

# This is used for test_cli.py::test_import_object_from_name
raise RuntimeError("This is a test exception")
from pprint import pprint
from typing import Optional
import msgspec
import pytest
from erdantic.core import EntityRelationshipDiagram, FullyQualifiedName
import erdantic.examples.msgspec as msgspec_examples
from erdantic.exceptions import UnresolvableForwardRefError
from erdantic.plugins.msgspec import (
get_fields_from_msgspec_struct,
is_msgspec_struct,
)
def test_is_msgspec_struct():
class NotAStruct:
pass
assert is_msgspec_struct(msgspec_examples.Party)
assert is_msgspec_struct(msgspec_examples.QuestGiver)
assert not is_msgspec_struct(
msgspec_examples.QuestGiver(name="Gandalf", faction=None, location="Shire")
)
assert not is_msgspec_struct("Hello")
assert not is_msgspec_struct(str)
assert not is_msgspec_struct(NotAStruct)
def test_get_fields_from_msgspec_struct():
fields = get_fields_from_msgspec_struct(msgspec_examples.Party)
assert len(fields) == 4
# name
assert fields[0].model_full_name == FullyQualifiedName.from_object(msgspec_examples.Party)
assert fields[0].name == "name"
assert fields[0].type_name == "str"
# formed_datetime
assert fields[1].model_full_name == FullyQualifiedName.from_object(msgspec_examples.Party)
assert fields[1].name == "formed_datetime"
assert fields[1].type_name == "datetime"
# members
assert fields[2].model_full_name == FullyQualifiedName.from_object(msgspec_examples.Party)
assert fields[2].name == "members"
assert fields[2].type_name == "List[Adventurer]"
# active_quest
assert fields[3].model_full_name == FullyQualifiedName.from_object(msgspec_examples.Party)
assert fields[3].name == "active_quest"
assert fields[3].type_name == "Optional[Quest]"
class GlobalOtherClassBefore(msgspec.Struct):
"""Another struct to be referenced as a forward reference. Defined before the struct that
references it."""
my_field: str
class GlobalWithFwdRefs(msgspec.Struct):
"""Globally defined struct with forward references. This should have no problems being
automatically resolved."""
# Resolved by resolve_types_on_dataclass in get_fields_from_msgspec_struct
imported_ref: "msgspec_examples.Party"
nested_imported_ref: Optional["msgspec_examples.Quest"]
self_ref: "GlobalWithFwdRefs"
nested_self_ref: Optional["GlobalWithFwdRefs"]
global_ref_before: "GlobalOtherClassBefore"
nested_global_ref_before: Optional["GlobalOtherClassBefore"]
global_ref_after: "GlobalOtherClassAfter"
nested_global_ref_after: Optional["GlobalOtherClassAfter"]
class GlobalOtherClassAfter(msgspec.Struct):
"""Another struct to be referenced as a forward reference. Defined after the struct that
references it."""
my_field: str
def test_forward_refs_global_scope():
"""Global scope struct with forward references that we can handle automatically."""
# Class is defined in the global scope
fields = {fi.name: fi for fi in get_fields_from_msgspec_struct(GlobalWithFwdRefs)}
pprint({name: (fi.type_name, fi.raw_type) for name, fi in fields.items()})
# Resolved by resolve_types_on_dataclass in get_fields_from_msgspec_struct
assert fields["imported_ref"].type_name == "Party"
assert fields["imported_ref"].raw_type == msgspec_examples.Party
assert fields["nested_imported_ref"].type_name == "Optional[Quest]"
assert fields["nested_imported_ref"].raw_type == Optional[msgspec_examples.Quest]
assert fields["self_ref"].type_name == "GlobalWithFwdRefs"
assert fields["self_ref"].raw_type == GlobalWithFwdRefs
assert fields["nested_self_ref"].type_name == "Optional[GlobalWithFwdRefs]"
assert fields["nested_self_ref"].raw_type == Optional[GlobalWithFwdRefs]
assert fields["global_ref_before"].type_name == "GlobalOtherClassBefore"
assert fields["global_ref_before"].raw_type == GlobalOtherClassBefore
assert fields["nested_global_ref_before"].type_name == "Optional[GlobalOtherClassBefore]"
assert fields["nested_global_ref_before"].raw_type == Optional[GlobalOtherClassBefore]
assert fields["global_ref_after"].type_name == "GlobalOtherClassAfter"
assert fields["global_ref_after"].raw_type == GlobalOtherClassAfter
assert fields["nested_global_ref_after"].type_name == "Optional[GlobalOtherClassAfter]"
assert fields["nested_global_ref_after"].raw_type == Optional[GlobalOtherClassAfter]
# Can add to diagram without error
diagram = EntityRelationshipDiagram()
diagram.add_model(GlobalWithFwdRefs)
def test_forward_refs_fn_scope_auto_resolvable():
"""Function scope struct with forward references that we can automatically resolve."""
# Resolved by resolve_types_on_dataclass in get_fields_from_msgspec_struct
class FnScopeAutomaticallyResolvable(msgspec.Struct):
imported_ref: "msgspec_examples.Party"
nested_imported_ref: Optional["msgspec_examples.Quest"]
global_ref: "GlobalOtherClassBefore"
nested_global_ref: Optional["GlobalOtherClassBefore"]
fields = {fi.name: fi for fi in get_fields_from_msgspec_struct(FnScopeAutomaticallyResolvable)}
pprint({name: (fi.type_name, fi.raw_type) for name, fi in fields.items()})
assert fields["imported_ref"].type_name == "Party"
assert fields["imported_ref"].raw_type == msgspec_examples.Party
assert fields["nested_imported_ref"].type_name == "Optional[Quest]"
assert fields["nested_imported_ref"].raw_type == Optional[msgspec_examples.Quest]
assert fields["global_ref"].type_name == "GlobalOtherClassBefore"
assert fields["global_ref"].raw_type == GlobalOtherClassBefore
assert fields["nested_global_ref"].type_name == "Optional[GlobalOtherClassBefore]"
assert fields["nested_global_ref"].raw_type == Optional[GlobalOtherClassBefore]
# Can add to diagram without error
diagram = EntityRelationshipDiagram()
diagram.add_model(FnScopeAutomaticallyResolvable)
def test_forward_refs_fn_scope_manual_resolvable():
"""Function scope model with forward references that we need to manually resolve."""
class FnScopeOtherClassBefore(msgspec.Struct):
"""Another class to be referenced as a forward reference. Defined before the class that
references it."""
my_field: str
class FnScopeManuallyResolvable(msgspec.Struct):
self_ref: "FnScopeManuallyResolvable"
nested_self_ref: Optional["FnScopeManuallyResolvable"]
sibling_ref_before: "FnScopeOtherClassBefore"
nested_sibling_ref_before: "Optional[FnScopeOtherClassBefore]"
sibling_ref_after: "FnScopeOtherClassAfter"
nested_sibling_ref_after: Optional["FnScopeOtherClassAfter"]
class FnScopeOtherClassAfter(msgspec.Struct):
"""Another class to be referenced as a forward reference. Defined after the class that
references it."""
my_field: str
with pytest.raises(UnresolvableForwardRefError, match="'FnScopeManuallyResolvable'"):
diagram = EntityRelationshipDiagram()
diagram.add_model(FnScopeManuallyResolvable)
with pytest.raises(UnresolvableForwardRefError, match="'FnScopeManuallyResolvable'"):
get_fields_from_msgspec_struct(FnScopeManuallyResolvable)
# Don't currently support manually resolving forward references for structs
# Call attrs.resolve_types() to manually resolve
# resolve_types_on_struct(FnScopeManuallyResolvable, localns=locals())
# Adding to diagram should now work without error
# diagram = EntityRelationshipDiagram()
# diagram.add_model(FnScopeManuallyResolvable)
# Fields should match expected
# fields = {fi.name: fi for fi in get_fields_from_msgspec_struct(FnScopeManuallyResolvable)}
# pprint({name: (fi.type_name, fi.raw_type) for name, fi in fields.items()})
# assert fields["self_ref"].type_name == "FnScopeManuallyResolvable"
# assert fields["self_ref"].raw_type == FnScopeManuallyResolvable
# assert fields["nested_self_ref"].type_name == "Optional[FnScopeManuallyResolvable]"
# assert fields["nested_self_ref"].raw_type == Optional[FnScopeManuallyResolvable]
# assert fields["sibling_ref_before"].type_name == "FnScopeOtherClassBefore"
# assert fields["sibling_ref_before"].raw_type == FnScopeOtherClassBefore
# assert fields["nested_sibling_ref_before"].type_name == "Optional[FnScopeOtherClassBefore]"
# assert fields["nested_sibling_ref_before"].raw_type == Optional[FnScopeOtherClassBefore]
# assert fields["sibling_ref_after"].type_name == "FnScopeOtherClassAfter"
# assert fields["sibling_ref_after"].raw_type == FnScopeOtherClassAfter
# assert fields["nested_sibling_ref_after"].type_name == "Optional[FnScopeOtherClassAfter]"
# assert fields["nested_sibling_ref_after"].raw_type == Optional[FnScopeOtherClassAfter]
+90
-88
name: release
run-name: Release of ${{ inputs.version }}${{ inputs.dry-run && ' (dry run)' }}
on:
release:
types:
- published
workflow_dispatch:
inputs:
version:
description: 'Version tag'
required: true
dry-run:
description: 'Dry run'
required: false
default: false
type: boolean
jobs:
build:
name: Build and publish new release
check-version:
name: Check metadata version is correct
runs-on: "ubuntu-latest"
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: conda-incubator/setup-miniconda@v3
- uses: prefix-dev/setup-pixi@v0.8.5
with:
miniforge-version: latest
activate-environment: ""
use-mamba: true
cache: true
cache-key: default
environments: default
- name: Install nox and uv
run: |
pipx install nox
pipx install uv
- name: Check that versions match
id: version
run: |
nox -s dev
echo "Release tag: [${{ github.event.release.tag_name }}]"
PACKAGE_VERSION=$(.nox/dev/bin/python -c "import erdantic; print(erdantic.__version__)")
echo "Input version tag: [${{ github.event.inputs.version }}] "
PACKAGE_VERSION=$(pixi run python -m vspect read .)
echo "Package version: [$PACKAGE_VERSION]"
[ ${{ github.event.release.tag_name }} == "v$PACKAGE_VERSION" ] || { exit 1; }
echo "full_version=v$PACKAGE_VERSION" >> $GITHUB_ENV
MAJOR_MINOR_VERSION=$([[ $PACKAGE_VERSION =~ ^[0-9]+\.[0-9]+ ]] && echo "${BASH_REMATCH[0]}")
echo "major_minor_version=$([[ $PACKAGE_VERSION =~ ^[0-9]+\.[0-9]+ ]] && echo "v${BASH_REMATCH[0]}")" >> $GITHUB_ENV
[[ ${{ github.event.inputs.version }} == "v$PACKAGE_VERSION" ]] || { exit 1; }
- name: Build package
run: |
nox -s build --verbose
build-and-inspect-package:
name: Build and inspect package
needs: check-version
runs-on: ubuntu-latest
environment: release
permissions:
# Need both of these for attest-build-provenance-github
id-token: write
attestations: write
- name: Build docs
run: |
nox -s docs --verbose
steps:
- uses: actions/checkout@v4
- uses: hynek/build-and-inspect-python-package@v2
with:
attest-build-provenance-github: 'true'
- name: Publish to Test PyPI
uses: pypa/gh-action-pypi-publish@v1.3.0
upload-to-test-pypi:
name: Upload package to PyPI
needs: build-and-inspect-package
if: github.event.inputs.dry-run == 'false'
runs-on: ubuntu-latest
environment: release
permissions:
id-token: write # Required for PyPI trusted publishing
steps:
- name: Download built artifact to dist/
uses: actions/download-artifact@v4
with:
user: ${{ secrets.PYPI_TEST_USERNAME }}
password: ${{ secrets.PYPI_TEST_PASSWORD }}
repository_url: https://test.pypi.org/legacy/
skip_existing: true
name: Packages
path: dist
- name: Publish to Production PyPI
uses: pypa/gh-action-pypi-publish@v1.3.0
- uses: pypa/gh-action-pypi-publish@v1.12.4
with:
user: ${{ secrets.PYPI_PROD_USERNAME }}
password: ${{ secrets.PYPI_PROD_PASSWORD }}
skip_existing: false
repository-url: https://test.pypi.org/legacy/
skip-existing: true
- name: Stage docs on gh-pages (release candidate)
if: contains(steps.version.outputs.full_version, 'rc')
working-directory: docs
run: |
git fetch origin gh-pages --depth=1
git config user.name github-actions[bot]
git config user.email 41898282+github-actions[bot]@users.noreply.github.com
RUN_MIKE="conda run -p ../.nox/docs mike"
$RUN_MIKE deploy --push \
${{ steps.version.outputs.major_minor_version }}rc \
--title="${{ steps.version.outputs.major_minor_version }}rc"
upload-to-prod-pypi:
name: Upload package to PyPI
needs: upload-to-test-pypi
if: github.event.inputs.dry-run == 'false'
runs-on: ubuntu-latest
environment: release
permissions:
id-token: write # Required for PyPI trusted publishing
- name: Stage docs on gh-pages (stable)
if: ${{ ! contains(steps.version.outputs.full_version, 'rc') }}
working-directory: docs
run: |
git fetch origin gh-pages --depth=1
git config user.name github-actions[bot]
git config user.email 41898282+github-actions[bot]@users.noreply.github.com
RUN_MIKE="conda run -p ../.nox/docs mike"
# Rename old stable version
$RUN_MIKE list -j | jq
OLD_STABLE=$($RUN_MIKE list -j | jq -r '.[] | select(.aliases | index("stable")) | .title' | awk '{print $1;}')
echo $OLD_STABLE
$RUN_MIKE retitle stable $OLD_STABLE
# Deploy new version as stable
$RUN_MIKE deploy --push --update-aliases \
${{ steps.version.outputs.major_minor_version }} \
stable \
--title="${{ github.event.release.tag_name }} (stable)"
steps:
- name: Download built artifact to dist/
uses: actions/download-artifact@v4
with:
name: Packages
path: dist
deploy-docs:
name: Deploy docs to Netlify
needs: build
runs-on: "ubuntu-latest"
- uses: pypa/gh-action-pypi-publish@v1.12.4
create-github-release:
name: Create GitHub release
needs: upload-to-prod-pypi
if: github.event.inputs.dry-run == 'false'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- id: extract-changelog
uses: sean0x42/markdown-extract@v2.1.0
with:
ref: gh-pages
file: CHANGELOG.md
pattern: ${{ github.event.inputs.version }}
- name: Deploy docs to Netlify
uses: nwtgck/actions-netlify@v1.1
- name: Write output to file
run: |
cat <<'__EOF__' > __CHANGELOG-extracted.md
${{ steps.extract-changelog.outputs.markdown }}
__EOF__
- uses: ncipollo/release-action@v1
with:
publish-dir: "./"
production-deploy: true
github-token: ${{ secrets.GITHUB_TOKEN }}
deploy-message: "Deploy from GitHub Actions"
enable-pull-request-comment: false
enable-commit-comment: false
overwrites-pull-request-comment: false
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
timeout-minutes: 1
tag: ${{ github.event.inputs.version }}
commit: main
artifacts: "dist/*.whl,dist/*.tar.gz"
bodyFile: "__CHANGELOG-extracted.md"

@@ -11,2 +11,8 @@ name: tests

workflow_dispatch:
inputs:
update-deps:
description: "Update test dependencies"
required: false
default: false
type: boolean

@@ -21,19 +27,19 @@ jobs:

- name: Set up Python
uses: actions/setup-python@v5
- uses: extractions/setup-just@v2
- uses: prefix-dev/setup-pixi@v0.8.5
with:
python-version: "3.11"
cache: true
cache-key: default
environments: >-
default
typecheck
- name: Install nox and uv
run: |
pipx install nox
pipx install uv
- name: Lint
run: |
nox -s lint --verbose
just lint
- name: Typecheck
run: |
nox -s typecheck --verbose
just typecheck

@@ -47,3 +53,3 @@ tests:

os: [ubuntu-latest, macos-latest]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]

@@ -53,16 +59,34 @@ steps:

- uses: conda-incubator/setup-miniconda@v3
- name: Get environment name
id: env_name
run: |
env_name="test-py$(echo ${{ matrix.python-version }} | tr -d .)"
echo "env_name=${env_name}" | tee -a "$GITHUB_OUTPUT"
- uses: extractions/setup-just@v2
- uses: prefix-dev/setup-pixi@v0.8.5
with:
miniforge-version: latest
activate-environment: ""
use-mamba: true
cache: true
cache-key: ${{ steps.env_name.outputs.env_name }}
environments: ${{ steps.env_name.outputs.env_name }}
- name: Install nox and uv
- name: Update dependencies
if: >-
${{
github.event_name == 'push'
|| github.event_name == 'scheduled'
|| (github.event_name == 'workflow_dispatch' && github.event.inputs.update-deps == 'true')
}}
run: |
pipx install nox
pipx install uv
pixi update -e ${{ steps.env_name.outputs.env_name }}
- name: Fix graphviz for macos-latest
if: ${{ matrix.os == 'macos-latest' }}
run: |
just python=${{ matrix.python-version }} fix-graphviz-test
- name: Run tests
run: |
nox -s tests-${{ matrix.python-version }} --verbose
just python=${{ matrix.python-version }} test

@@ -76,3 +100,2 @@ - name: Upload test outputs

- name: Upload coverage to codecov

@@ -83,77 +106,40 @@ uses: codecov/codecov-action@v3

file: ./coverage.xml
fail_ci_if_error: true
fail_ci_if_error: ${{ (github.event_name == 'push' && true) || (github.event_name == 'pull_request' && true) || false }}
if: ${{ matrix.os == 'ubuntu-latest' }}
test-build-and-install:
name: Build distribution and test installation
test-distribution:
name: Test distribution
needs: code-quality
runs-on: ubuntu-latest
defaults:
run:
shell: bash -leo pipefail {0} # needed for micromamba environment
steps:
- uses: actions/checkout@v4
- uses: conda-incubator/setup-miniconda@v3
- uses: mamba-org/setup-micromamba@v2
with:
miniforge-version: latest
activate-environment: ""
use-mamba: true
environment-name: test-env
create-args: >-
python=3.13
graphviz
- name: Install nox and uv
run: |
pipx install nox
pipx install uv
- name: Build distribution
run: |
nox -s build
- name: Test wheel with extras matrix
run: |
nox -s test_wheel --verbose
- name: Test sdist
run: |
nox -s test_sdist --verbose
docs:
name: Docs preview
needs: code-quality
if: github.event.pull_request != null
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: conda-incubator/setup-miniconda@v3
- uses: astral-sh/setup-uv@v5
with:
miniforge-version: latest
activate-environment: ""
use-mamba: true
enable-cache: true
cache-suffix: "test-distr"
- name: Install nox and uv
- name: Install
run: |
pipx install nox
pipx install uv
uv pip install .
- name: Test building documentation
- name: Test
run: |
nox -s docs --verbose
python -m erdantic --version
python -m erdantic --list-plugins
python -c "import erdantic; import erdantic.examples; print(erdantic.list_plugins())"
- name: Deploy site preview to Netlify
uses: nwtgck/actions-netlify@v1.1
with:
publish-dir: "./docs/site"
production-deploy: false
github-token: ${{ secrets.GITHUB_TOKEN }}
deploy-message: "Deploy from GitHub Actions"
enable-pull-request-comment: true
enable-commit-comment: false
overwrites-pull-request-comment: true
alias: deploy-preview-${{ github.event.number }}
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
timeout-minutes: 1
notify:
name: Notify failed build
needs: [code-quality, tests, test-build-and-install, docs]
needs: [code-quality, tests, test-distribution]
if: failure() && github.event.pull_request == null

@@ -160,0 +146,0 @@ runs-on: ubuntu-latest

@@ -143,1 +143,5 @@ docs/docs/examples/

.pyre/
# pixi environments
.pixi
*.egg-info
# Contributing to erdantic
...
## Report a bug or request a feature
Please file an issue in the [issue tracker](https://github.com/drivendataorg/erdantic/issues).
## External contributions
Pull requests from external contributors are welcome. However, we ask that any nontrivial changes be discussed with maintainers in an [issue](https://github.com/drivendataorg/erdantic/issues) first before submitting a pull request.
## Local development
[![Nox](https://img.shields.io/badge/%F0%9F%A6%8A-Nox-D85E00.svg)](https://github.com/wntrblm/nox)
[![Pixi Badge][https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/prefix-dev/pixi/main/assets/badge/v0.json&style=flat-square
]][[pixi-url](https://pixi.sh)]
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://docs.astral.sh/ruff/)
[![Checked with mypy](https://www.mypy-lang.org/static/mypy_badge.svg)](https://mypy-lang.org/)
This project is set up using [nox](https://nox.thea.codes/en/stable/) for automation. We recommend you install nox as a global tool with [pipx](https://pipx.pypa.io/):
This project is set up using [Pixi](https://pixi.sh) for managing environments and [Just](https://github.com/casey/just) as a task runner.
Many useful recipes are defined in the [`justfile`](./justfile). You can run:
```bash
pipx install nox
just
```
Many of the nox sessions are configured to use conda environments. erdantic depends on [graphviz](https://graphviz.org/), a C library, so conda is handy for installing it together alongside the Python dependencies. We recommend you install conda through the [miniforge](https://github.com/conda-forge/miniforge) distribution, which automatically comes with the faster mamba installer.
to see available recipes with brief documentation.
### Development environment
To create a development environment, run:
Pixi handles the creation and synchronization of the default development environment. You can run commands in the environment like so:
```bash
nox -s dev
```
```bash
pixi run {some shell command}
```
This will create a conda environment in `.nox/dev`. You can activate it with:
You can activate the default environment with
```bash
conda activate .nox/dev
pixi shell
```

@@ -35,12 +44,16 @@

To run the full test matrix on all Python versions, run:
```bash
just test
```
You can run the tests for a specific Python version with, for example:
```bash
nox -s tests
just python=3.12 test
```
To run tests for a specific version, use `tests-{python-version}`, e.g.,
To run tests on all supported Python versions, run:
```bash
nox -s tests-3.11
just test-all
```

@@ -50,7 +63,7 @@

We use [ruff](https://docs.astral.sh/ruff/) for linting and formatting, and we use [mypy](https://github.com/python/mypy) for type-checking. You can run them with the following nox sessions:
We use [ruff](https://docs.astral.sh/ruff/) for linting and formatting, and we use [mypy](https://github.com/python/mypy) for static type checking. You can run them with the following commands:
```bash
nox -s lint
nox -s typecheck
just lint
just typecheck
```
# erdantic.examples.pydantic
::: erdantic.examples.pydantic
::: erdantic.examples.pydantic_v1
selection:

@@ -5,0 +5,0 @@ filters:

@@ -26,3 +26,3 @@ import inspect

readme_text = readme_text.replace(
"./HISTORY.md",
"./CHANGELOG.md",
"changelog.md",

@@ -34,3 +34,3 @@ )

def _read_changelog():
changelog_path = REPO_ROOT / "HISTORY.md"
changelog_path = REPO_ROOT / "CHANGELOG.md"
logger.info("Reading CHANGELOG from %s", changelog_path)

@@ -37,0 +37,0 @@ changelog_text = changelog_path.read_text()

@@ -18,2 +18,3 @@ site_name: erdantic

- "Usage Example: dataclasses": "examples/dataclasses.ipynb"
- "Usage Example: msgspec": "examples/msgspec.ipynb"
- "Usage Example: Pydantic": "examples/pydantic.ipynb"

@@ -31,2 +32,3 @@ - Advanced Usage:

- erdantic.examples.dataclasses: "api-reference/examples/dataclasses.md"
- erdantic.examples.msgspec: "api-reference/examples/msgspec.md"
- erdantic.examples.pydantic: "api-reference/examples/pydantic.md"

@@ -38,2 +40,3 @@ - erdantic.examples.pydantic_v1: "api-reference/examples/pydantic_v1.md"

- erdantic.plugins.dataclasses: "api-reference/plugins/dataclasses.md"
- erdantic.plugins.msgspec: "api-reference/plugins/msgspec.md"
- erdantic.plugins.pydantic: "api-reference/plugins/pydantic.md"

@@ -65,3 +68,5 @@ - erdantic.typing_utils: "api-reference/typing_utils.md"

plugins:
- search
- search:
- autorefs:
resolve_closest: true
- mkdocs-jupyter:

@@ -68,0 +73,0 @@ execute: false

@@ -1,11 +0,4 @@

import sys
from functools import cache
from typing import Type
if sys.version_info >= (3, 9):
from functools import cache
else:
from functools import lru_cache
cache = lru_cache(maxsize=None)
from pydantic import BaseModel

@@ -12,0 +5,0 @@

@@ -5,11 +5,5 @@ from enum import Enum

from pathlib import Path
import sys
from types import ModuleType
from typing import TYPE_CHECKING, List, Optional, Union
from typing import TYPE_CHECKING, Annotated, List, Optional, Union
if sys.version_info >= (3, 9):
from typing import Annotated
else:
from typing_extensions import Annotated
import typer

@@ -21,3 +15,3 @@

from erdantic.exceptions import ModelOrModuleNotFoundError
from erdantic.plugins import list_plugins
import erdantic.plugins

@@ -40,3 +34,5 @@ app = typer.Typer()

else:
AvailablePluginKeys = StrEnum("AvailablePluginKeys", {key: key for key in list_plugins()})
AvailablePluginKeys = StrEnum(
"AvailablePluginKeys", {key: key for key in erdantic.plugins.list_plugins()}
)

@@ -60,2 +56,23 @@

def list_plugins_callback(list_plugins: bool):
if list_plugins:
active_plugins = erdantic.plugins.list_plugins()
core_plugins = [plugin for plugin, _ in erdantic.plugins.CORE_PLUGINS]
print(" " * 15 + "ACTIVE PLUGIN NAME")
print("Core plugins:")
for plugin in core_plugins:
if plugin in active_plugins:
print(" " * 15 + f" [X] {plugin}")
else:
print(" " * 15 + f" [ ] {plugin}")
other_plugins = sorted(set(active_plugins) - set(core_plugins))
print("Other plugins:")
if other_plugins:
for plugin in other_plugins:
print(" " * 15 + f" [X] {plugin}")
else:
print(" " * 15 + " None found")
raise typer.Exit()
@app.command()

@@ -146,2 +163,11 @@ def main(

] = 0,
list_plugins: Annotated[
Optional[bool],
typer.Option(
"--list-plugins",
callback=list_plugins_callback,
is_eager=True,
help="List active plugins and exit.",
),
] = False,
version: Annotated[

@@ -169,3 +195,3 @@ Optional[bool],

logger.debug("Registered plugins: %s", ", ".join(list_plugins()))
logger.debug("Registered plugins: %s", ", ".join(erdantic.plugins.list_plugins()))

@@ -172,0 +198,0 @@ logger.debug("models_or_modules: %s", models_or_modules)

@@ -17,3 +17,3 @@ from enum import Enum

import pydantic
import pygraphviz as pgv # type: ignore [import-not-found]
import pygraphviz as pgv # type: ignore [import-untyped, import-not-found]
from sortedcontainers_pydantic import SortedDict

@@ -495,3 +495,3 @@ from typenames import REMOVE_ALL_MODULES, typenames

EntityRelationshipDiagram class, this is erdantic.core.ModelInfo."""
annotation = self.model_fields["models"].annotation
annotation = type(self).model_fields["models"].annotation
args = get_args(annotation)

@@ -504,3 +504,3 @@ return args[1]

EntityRelationshipDiagram class, this is erdantic.core.Edge."""
annotation = self.model_fields["edges"].annotation
annotation = type(self).model_fields["edges"].annotation
args = get_args(annotation)

@@ -527,3 +527,3 @@ return args[1]

self.models[key] = model_info
logger.debug("Sucessfully added model '%s'.", key)
logger.debug("Successfully added model '%s'.", key)
if recurse:

@@ -530,0 +530,0 @@ logger.debug("Searching fields of '%s' for other models...", key)

@@ -20,3 +20,9 @@ import importlib.metadata

CORE_PLUGINS = ("pydantic", "attrs", "dataclasses")
CORE_PLUGINS = (
("pydantic", "erdantic.plugins.pydantic"),
("pydantic_v1", "erdantic.plugins.pydantic"),
("attrs", "erdantic.plugins.attrs"),
("dataclasses", "erdantic.plugins.dataclasses"),
("msgspec", "erdantic.plugins.msgspec"),
)

@@ -29,6 +35,6 @@ _ModelType = TypeVar("_ModelType", bound=type)

def load_plugins():
for plugin in CORE_PLUGINS:
for plugin, module in CORE_PLUGINS:
logger.debug("Loading plugin: %s", plugin)
try:
importlib.import_module(f"erdantic.plugins.{plugin}")
importlib.import_module(module)
logger.debug("Plugin successfully loaded: %s", plugin)

@@ -35,0 +41,0 @@ except ModuleNotFoundError:

import dataclasses
import re
import sys
from typing import TYPE_CHECKING, Any, List, Type, get_type_hints
from typing import TYPE_CHECKING, Any, List, Type, cast, get_type_hints
if sys.version_info >= (3, 9):
# include_extras was added in Python 3.9
from typing import get_type_hints
else:
from typing_extensions import get_type_hints
if sys.version_info >= (3, 10):

@@ -72,3 +66,3 @@ from typing import TypeGuard

name=f.name,
raw_type=f.type,
raw_type=cast(type, f.type), # cast narrows type for typechecking
)

@@ -75,0 +69,0 @@ for f in dataclasses.fields(model)

@@ -1,4 +0,4 @@

Metadata-Version: 2.3
Metadata-Version: 2.4
Name: erdantic
Version: 1.0.5
Version: 1.1.0
Summary: Entity relationship diagrams for Python data model classes like Pydantic.

@@ -13,3 +13,3 @@ Project-URL: Repository, https://github.com/drivendataorg/erdantic

License-File: LICENSE
Keywords: attrs,dataclasses,entity relationship diagram,erd,pydantic
Keywords: attrs,dataclasses,entity relationship diagram,erd,msgspec,pydantic
Classifier: Framework :: Pydantic

@@ -21,3 +21,2 @@ Classifier: Framework :: Pydantic :: 2

Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9

@@ -27,4 +26,5 @@ Classifier: Programming Language :: Python :: 3.10

Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Code Generators
Requires-Python: >=3.8
Requires-Python: >=3.9
Requires-Dist: pydantic-core

@@ -39,2 +39,4 @@ Requires-Dist: pydantic>=2

Requires-Dist: attrs; extra == 'attrs'
Provides-Extra: msgspec
Requires-Dist: msgspec; extra == 'msgspec'
Description-Content-Type: text/markdown

@@ -51,5 +53,2 @@

> [!NOTE]
> erdantic v1.0 has been released! See the [changelog](./HISTORY.md) for more information.
**erdantic** is a simple tool for drawing [entity relationship diagrams (ERDs)](https://en.wikipedia.org/wiki/Data_modeling#Entity%E2%80%93relationship_diagrams) for Python data model classes. Diagrams are rendered using the venerable [Graphviz](https://graphviz.org/) library. Supported data modeling frameworks are:

@@ -60,2 +59,3 @@

- [attrs](https://www.attrs.org/en/stable/)
- [msgspec](https://jcristharif.com/msgspec/)
- [dataclasses](https://docs.python.org/3/library/dataclasses.html) from the Python standard library

@@ -62,0 +62,0 @@

@@ -1,8 +0,4 @@

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "erdantic"
version = "1.0.5"
version = "1.1.0"
description = "Entity relationship diagrams for Python data model classes like Pydantic."

@@ -12,3 +8,3 @@ readme = "README.md"

license = { text = "MIT License" }
keywords = ["erd", "entity relationship diagram", "dataclasses", "pydantic", "attrs"]
keywords = ["erd", "entity relationship diagram", "dataclasses", "pydantic", "attrs", "msgspec"]
classifiers = [

@@ -20,3 +16,2 @@ "Intended Audience :: Developers",

"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",

@@ -26,6 +21,7 @@ "Programming Language :: Python :: 3.10",

"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Framework :: Pydantic",
"Framework :: Pydantic :: 2",
]
requires-python = ">=3.8"
requires-python = ">=3.9"
dependencies = [

@@ -43,2 +39,3 @@ "pydantic >= 2",

attrs = ["attrs"]
msgspec = ["msgspec"]

@@ -54,2 +51,76 @@ [project.scripts]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
## DEV
[dependency-groups]
dev = [
{ include-group = "lint" },
{ include-group = "docs" },
"build",
"ipython",
"vspect",
]
docs = [
"black",
"markdown-callouts>=0.4.0",
"mdx_truly_sane_lists==1.3",
"mike",
"mkdocs>=1.4",
"mkdocs-jupyter",
"mkdocs-material>=7.2.6",
"mkdocstrings[python]>=0.19.0",
"nbconvert>=7.7.0",
"rich",
]
lint = [
"ruff>=0.1.14",
]
test = [
"filetype",
"ipython",
"pytest<8",
"pytest-cases",
"pytest-cov",
"rich",
]
typecheck = [
"mypy",
"pip",
]
[tool.pixi.project]
channels = ["conda-forge"]
platforms = ["linux-64", "osx-64", "osx-arm64", "win-64"]
[tool.pixi.dependencies]
graphviz = "==12.2.1"
[tool.pixi.pypi-dependencies]
erdantic = { path = ".", editable = true, extras = ["attrs", "msgspec"] }
[tool.pixi.feature.py39.dependencies]
python = "3.9.*"
[tool.pixi.feature.py310.dependencies]
python = "3.10.*"
[tool.pixi.feature.py311.dependencies]
python = "3.11.*"
[tool.pixi.feature.py312.dependencies]
python = "3.12.*"
[tool.pixi.feature.py313.dependencies]
python = "3.13.*"
[tool.pixi.environments]
default = ["py313", "dev"]
test-py39 = ["py39", "test"]
test-py310 = ["py310", "test"]
test-py311 = ["py311", "test"]
test-py312 = ["py312", "test"]
test-py313 = ["py313", "test"]
typecheck = ["py313", "typecheck"]
## TOOLS
[tool.ruff]

@@ -56,0 +127,0 @@ line-length = 99

@@ -10,5 +10,2 @@ # erdantic: Entity Relationship Diagrams

> [!NOTE]
> erdantic v1.0 has been released! See the [changelog](./HISTORY.md) for more information.
**erdantic** is a simple tool for drawing [entity relationship diagrams (ERDs)](https://en.wikipedia.org/wiki/Data_modeling#Entity%E2%80%93relationship_diagrams) for Python data model classes. Diagrams are rendered using the venerable [Graphviz](https://graphviz.org/) library. Supported data modeling frameworks are:

@@ -19,2 +16,3 @@

- [attrs](https://www.attrs.org/en/stable/)
- [msgspec](https://jcristharif.com/msgspec/)
- [dataclasses](https://docs.python.org/3/library/dataclasses.html) from the Python standard library

@@ -21,0 +19,0 @@

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 9.0.0 (0)
<!-- Generated by graphviz version 12.2.1 (0)
-->
<!-- Title: Entity Relationship Diagram created by erdantic Pages: 1 -->
<svg width="686pt" height="309pt"
viewBox="0.00 0.00 686.00 308.50" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 304.5)">
<svg width="689pt" height="314pt"
viewBox="0.00 0.00 689.00 313.75" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 309.75)">
<title>Entity Relationship Diagram created by erdantic</title>
<polygon fill="white" stroke="none" points="-4,4 -4,-304.5 682,-304.5 682,4 -4,4"/>
<text text-anchor="middle" x="339" y="-5.95" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="9.00" fill="#a8a8a8">Created by erdantic vTEST &lt;https://github.com/drivendataorg/erdantic&gt;</text>
<polygon fill="white" stroke="none" points="-4,4 -4,-309.75 685,-309.75 685,4 -4,4"/>
<text text-anchor="middle" x="340.5" y="-5.95" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="9.00" fill="#a8a8a8">Created by erdantic vTEST &lt;https://github.com/drivendataorg/erdantic&gt;</text>
<!-- erdantic.examples.attrs.Adventurer -->

@@ -17,20 +17,20 @@ <g id="node1" class="node">

<g id="a_node1"><a xlink:title="erdantic.examples.attrs.Adventurer&#10;&#10;A person often late for dinner but with a tale or two to tell.&#10;&#10;Attributes:&#10; &#160;&#160;&#160;name (str): Name of this adventurer&#10; &#160;&#160;&#160;profession (str): Profession of this adventurer&#10; &#160;&#160;&#160;level (int): Level of this adventurer&#10; &#160;&#160;&#160;alignment (Alignment): Alignment of this adventurer&#10;">
<polygon fill="none" stroke="black" points="312,-274.5 312,-296.5 438,-296.5 438,-274.5 312,-274.5"/>
<text text-anchor="start" x="341.25" y="-281.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-weight="bold" font-size="14.00">Adventurer</text>
<polygon fill="none" stroke="black" points="312,-252.5 312,-274.5 374,-274.5 374,-252.5 312,-252.5"/>
<text text-anchor="start" x="328" y="-258.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">name</text>
<polygon fill="none" stroke="black" points="374,-252.5 374,-274.5 438,-274.5 438,-252.5 374,-252.5"/>
<text text-anchor="start" x="399.25" y="-258.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">str</text>
<polygon fill="none" stroke="black" points="312,-230.5 312,-252.5 374,-252.5 374,-230.5 312,-230.5"/>
<text text-anchor="start" x="314.88" y="-236.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">profession</text>
<polygon fill="none" stroke="black" points="374,-230.5 374,-252.5 438,-252.5 438,-230.5 374,-230.5"/>
<text text-anchor="start" x="399.25" y="-236.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">str</text>
<polygon fill="none" stroke="black" points="312,-208.5 312,-230.5 374,-230.5 374,-208.5 312,-208.5"/>
<text text-anchor="start" x="315.62" y="-214.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">alignment</text>
<polygon fill="none" stroke="black" points="374,-208.5 374,-230.5 438,-230.5 438,-208.5 374,-208.5"/>
<text text-anchor="start" x="376.75" y="-214.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">Alignment</text>
<polygon fill="none" stroke="black" points="312,-186.5 312,-208.5 374,-208.5 374,-186.5 312,-186.5"/>
<text text-anchor="start" x="329.88" y="-192.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">level</text>
<polygon fill="none" stroke="black" points="374,-186.5 374,-208.5 438,-208.5 438,-186.5 374,-186.5"/>
<text text-anchor="start" x="398.88" y="-192.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">int</text>
<polygon fill="none" stroke="black" points="313.12,-279.25 313.12,-301.75 439.88,-301.75 439.88,-279.25 313.12,-279.25"/>
<text text-anchor="start" x="342.75" y="-286.45" font-family="Times New Roman,Times,Liberation Serif,serif" font-weight="bold" font-size="14.00">Adventurer</text>
<polygon fill="none" stroke="black" points="313.12,-256.75 313.12,-279.25 375.38,-279.25 375.38,-256.75 313.12,-256.75"/>
<text text-anchor="start" x="329.25" y="-262.95" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">name</text>
<polygon fill="none" stroke="black" points="375.38,-256.75 375.38,-279.25 439.88,-279.25 439.88,-256.75 375.38,-256.75"/>
<text text-anchor="start" x="400.88" y="-262.95" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">str</text>
<polygon fill="none" stroke="black" points="313.12,-234.25 313.12,-256.75 375.38,-256.75 375.38,-234.25 313.12,-234.25"/>
<text text-anchor="start" x="316.12" y="-240.45" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">profession</text>
<polygon fill="none" stroke="black" points="375.38,-234.25 375.38,-256.75 439.88,-256.75 439.88,-234.25 375.38,-234.25"/>
<text text-anchor="start" x="400.88" y="-240.45" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">str</text>
<polygon fill="none" stroke="black" points="313.12,-211.75 313.12,-234.25 375.38,-234.25 375.38,-211.75 313.12,-211.75"/>
<text text-anchor="start" x="316.88" y="-217.95" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">alignment</text>
<polygon fill="none" stroke="black" points="375.38,-211.75 375.38,-234.25 439.88,-234.25 439.88,-211.75 375.38,-211.75"/>
<text text-anchor="start" x="378.38" y="-217.95" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">Alignment</text>
<polygon fill="none" stroke="black" points="313.12,-189.25 313.12,-211.75 375.38,-211.75 375.38,-189.25 313.12,-189.25"/>
<text text-anchor="start" x="331.12" y="-195.45" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">level</text>
<polygon fill="none" stroke="black" points="375.38,-189.25 375.38,-211.75 439.88,-211.75 439.88,-189.25 375.38,-189.25"/>
<text text-anchor="start" x="400.5" y="-195.45" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">int</text>
</a>

@@ -43,20 +43,20 @@ </g>

<g id="a_node2"><a xlink:title="erdantic.examples.attrs.Party&#10;&#10;A group of adventurers finding themselves doing and saying things altogether unexpected.&#10;&#10;Attributes:&#10; &#160;&#160;&#160;name (str): Name that party is known by&#10; &#160;&#160;&#160;formed_datetime (datetime): Timestamp of when the party was formed&#10; &#160;&#160;&#160;members (List[Adventurer]): Adventurers that belong to this party&#10; &#160;&#160;&#160;active_quest (Optional[Quest]): Current quest that party is actively tackling&#10;">
<polygon fill="none" stroke="black" points="0,-278.5 0,-300.5 196,-300.5 196,-278.5 0,-278.5"/>
<text text-anchor="start" x="81.88" y="-285.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-weight="bold" font-size="14.00">Party</text>
<polygon fill="none" stroke="black" points="0,-256.5 0,-278.5 99,-278.5 99,-256.5 0,-256.5"/>
<text text-anchor="start" x="34.5" y="-262.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">name</text>
<polygon fill="none" stroke="black" points="99,-256.5 99,-278.5 196,-278.5 196,-256.5 99,-256.5"/>
<text text-anchor="start" x="140.75" y="-262.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">str</text>
<polygon fill="none" stroke="black" points="0,-234.5 0,-256.5 99,-256.5 99,-234.5 0,-234.5"/>
<text text-anchor="start" x="2.62" y="-240.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">formed_datetime</text>
<polygon fill="none" stroke="black" points="99,-234.5 99,-256.5 196,-256.5 196,-234.5 99,-234.5"/>
<text text-anchor="start" x="123.88" y="-240.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">datetime</text>
<polygon fill="none" stroke="black" points="0,-212.5 0,-234.5 99,-234.5 99,-212.5 0,-212.5"/>
<text text-anchor="start" x="24" y="-218.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">members</text>
<polygon fill="none" stroke="black" points="99,-212.5 99,-234.5 196,-234.5 196,-212.5 99,-212.5"/>
<text text-anchor="start" x="101.75" y="-218.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">List[Adventurer]</text>
<polygon fill="none" stroke="black" points="0,-190.5 0,-212.5 99,-212.5 99,-190.5 0,-190.5"/>
<text text-anchor="start" x="15.75" y="-196.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">active_quest</text>
<polygon fill="none" stroke="black" points="99,-190.5 99,-212.5 196,-212.5 196,-190.5 99,-190.5"/>
<text text-anchor="start" x="103.62" y="-196.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">Optional[Quest]</text>
<polygon fill="none" stroke="black" points="0,-283.25 0,-305.75 197.25,-305.75 197.25,-283.25 0,-283.25"/>
<text text-anchor="start" x="82.5" y="-290.45" font-family="Times New Roman,Times,Liberation Serif,serif" font-weight="bold" font-size="14.00">Party</text>
<polygon fill="none" stroke="black" points="0,-260.75 0,-283.25 99.75,-283.25 99.75,-260.75 0,-260.75"/>
<text text-anchor="start" x="34.88" y="-266.95" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">name</text>
<polygon fill="none" stroke="black" points="99.75,-260.75 99.75,-283.25 197.25,-283.25 197.25,-260.75 99.75,-260.75"/>
<text text-anchor="start" x="141.75" y="-266.95" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">str</text>
<polygon fill="none" stroke="black" points="0,-238.25 0,-260.75 99.75,-260.75 99.75,-238.25 0,-238.25"/>
<text text-anchor="start" x="3" y="-244.45" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">formed_datetime</text>
<polygon fill="none" stroke="black" points="99.75,-238.25 99.75,-260.75 197.25,-260.75 197.25,-238.25 99.75,-238.25"/>
<text text-anchor="start" x="124.88" y="-244.45" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">datetime</text>
<polygon fill="none" stroke="black" points="0,-215.75 0,-238.25 99.75,-238.25 99.75,-215.75 0,-215.75"/>
<text text-anchor="start" x="24.38" y="-221.95" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">members</text>
<polygon fill="none" stroke="black" points="99.75,-215.75 99.75,-238.25 197.25,-238.25 197.25,-215.75 99.75,-215.75"/>
<text text-anchor="start" x="102.75" y="-221.95" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">List[Adventurer]</text>
<polygon fill="none" stroke="black" points="0,-193.25 0,-215.75 99.75,-215.75 99.75,-193.25 0,-193.25"/>
<text text-anchor="start" x="16.12" y="-199.45" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">active_quest</text>
<polygon fill="none" stroke="black" points="99.75,-193.25 99.75,-215.75 197.25,-215.75 197.25,-193.25 99.75,-193.25"/>
<text text-anchor="start" x="104.62" y="-199.45" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">Optional[Quest]</text>
</a>

@@ -68,7 +68,7 @@ </g>

<title>erdantic.examples.attrs.Party:e&#45;&gt;erdantic.examples.attrs.Adventurer:w</title>
<path fill="none" stroke="black" d="M205.92,-224.15C249.25,-230.11 255.51,-274.88 295.89,-283.91"/>
<polyline fill="none" stroke="black" points="196,-223.5 200.99,-223.83"/>
<polyline fill="none" stroke="black" points="200.99,-223.83 205.98,-224.16"/>
<polygon fill="black" stroke="black" points="300.72,-284.42 310.2,-289.94 306.03,-284.98 310.33,-285.43 310.33,-285.43 310.33,-285.43 306.03,-284.98 311.14,-280.99 300.72,-284.42"/>
<polyline fill="none" stroke="black" points="299.51,-284.29 294.54,-283.77"/>
<path fill="none" stroke="black" d="M207.21,-227.67C250.68,-233.77 256.49,-279.62 296.96,-288.88"/>
<polyline fill="none" stroke="black" points="197.25,-227 202.24,-227.34"/>
<polyline fill="none" stroke="black" points="202.24,-227.34 207.23,-227.67"/>
<polygon fill="black" stroke="black" points="301.85,-289.4 311.31,-294.94 307.15,-289.97 311.46,-290.43 311.46,-290.43 311.46,-290.43 307.15,-289.97 312.27,-285.99 301.85,-289.4"/>
<polyline fill="none" stroke="black" points="300.64,-289.27 295.67,-288.74"/>
</g>

@@ -79,16 +79,16 @@ <!-- erdantic.examples.attrs.Quest -->

<g id="a_node3"><a xlink:title="erdantic.examples.attrs.Quest&#10;&#10;A task to complete, with some monetary reward.&#10;&#10;Attributes:&#10; &#160;&#160;&#160;name (str): Name by which this quest is referred to&#10; &#160;&#160;&#160;giver (QuestGiver): Person who offered the quest&#10; &#160;&#160;&#160;reward_gold (int): Amount of gold to be rewarded for quest completion&#10;">
<polygon fill="none" stroke="black" points="304,-128.5 304,-150.5 446,-150.5 446,-128.5 304,-128.5"/>
<text text-anchor="start" x="357.75" y="-135.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-weight="bold" font-size="14.00">Quest</text>
<polygon fill="none" stroke="black" points="304,-106.5 304,-128.5 378,-128.5 378,-106.5 304,-106.5"/>
<text text-anchor="start" x="326" y="-112.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">name</text>
<polygon fill="none" stroke="black" points="378,-106.5 378,-128.5 446,-128.5 446,-106.5 378,-106.5"/>
<text text-anchor="start" x="405.25" y="-112.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">str</text>
<polygon fill="none" stroke="black" points="304,-84.5 304,-106.5 378,-106.5 378,-84.5 304,-84.5"/>
<text text-anchor="start" x="327.12" y="-90.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">giver</text>
<polygon fill="none" stroke="black" points="378,-84.5 378,-106.5 446,-106.5 446,-84.5 378,-84.5"/>
<text text-anchor="start" x="380.88" y="-90.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">QuestGiver</text>
<polygon fill="none" stroke="black" points="304,-62.5 304,-84.5 378,-84.5 378,-62.5 304,-62.5"/>
<text text-anchor="start" x="306.88" y="-68.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">reward_gold</text>
<polygon fill="none" stroke="black" points="378,-62.5 378,-84.5 446,-84.5 446,-62.5 378,-62.5"/>
<text text-anchor="start" x="404.88" y="-68.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">int</text>
<polygon fill="none" stroke="black" points="305.25,-131 305.25,-153.5 447.75,-153.5 447.75,-131 305.25,-131"/>
<text text-anchor="start" x="359.25" y="-138.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-weight="bold" font-size="14.00">Quest</text>
<polygon fill="none" stroke="black" points="305.25,-108.5 305.25,-131 379.5,-131 379.5,-108.5 305.25,-108.5"/>
<text text-anchor="start" x="327.38" y="-114.7" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">name</text>
<polygon fill="none" stroke="black" points="379.5,-108.5 379.5,-131 447.75,-131 447.75,-108.5 379.5,-108.5"/>
<text text-anchor="start" x="406.88" y="-114.7" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">str</text>
<polygon fill="none" stroke="black" points="305.25,-86 305.25,-108.5 379.5,-108.5 379.5,-86 305.25,-86"/>
<text text-anchor="start" x="328.5" y="-92.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">giver</text>
<polygon fill="none" stroke="black" points="379.5,-86 379.5,-108.5 447.75,-108.5 447.75,-86 379.5,-86"/>
<text text-anchor="start" x="382.5" y="-92.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">QuestGiver</text>
<polygon fill="none" stroke="black" points="305.25,-63.5 305.25,-86 379.5,-86 379.5,-63.5 305.25,-63.5"/>
<text text-anchor="start" x="308.25" y="-69.7" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">reward_gold</text>
<polygon fill="none" stroke="black" points="379.5,-63.5 379.5,-86 447.75,-86 447.75,-63.5 379.5,-63.5"/>
<text text-anchor="start" x="406.5" y="-69.7" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">int</text>
</a>

@@ -100,9 +100,9 @@ </g>

<title>erdantic.examples.attrs.Party:e&#45;&gt;erdantic.examples.attrs.Quest:w</title>
<path fill="none" stroke="black" d="M205.73,-200.8C245.01,-194.92 251.04,-153.51 285.33,-142.29"/>
<polyline fill="none" stroke="black" points="196,-201.5 200.99,-201.14"/>
<polyline fill="none" stroke="black" points="200.99,-201.14 205.97,-200.79"/>
<polyline fill="none" stroke="black" points="304,-139.5 299.06,-140.24"/>
<polygon fill="black" stroke="black" points="297.33,-135.44 298.81,-145.33 296.83,-145.63 295.35,-135.74 297.33,-135.44"/>
<polyline fill="none" stroke="black" points="299.06,-140.24 294.11,-140.98"/>
<ellipse fill="none" stroke="black" cx="289.66" cy="-141.65" rx="4" ry="4"/>
<path fill="none" stroke="black" d="M206.98,-203.8C246.3,-197.9 252.26,-156.32 286.57,-145.06"/>
<polyline fill="none" stroke="black" points="197.25,-204.5 202.24,-204.14"/>
<polyline fill="none" stroke="black" points="202.24,-204.14 207.22,-203.78"/>
<polyline fill="none" stroke="black" points="305.25,-142.25 300.31,-142.99"/>
<polygon fill="black" stroke="black" points="298.57,-138.2 300.06,-148.09 298.08,-148.38 296.6,-138.49 298.57,-138.2"/>
<polyline fill="none" stroke="black" points="300.31,-142.99 295.36,-143.74"/>
<ellipse fill="none" stroke="black" cx="290.91" cy="-144.4" rx="4" ry="4"/>
</g>

@@ -113,16 +113,16 @@ <!-- erdantic.examples.attrs.QuestGiver -->

<g id="a_node4"><a xlink:title="erdantic.examples.attrs.QuestGiver&#10;&#10;A person who offers a task that needs completing.&#10;&#10;Attributes:&#10; &#160;&#160;&#160;name (str): Name of this quest giver&#10; &#160;&#160;&#160;faction (str): Faction that this quest giver belongs to&#10; &#160;&#160;&#160;location (str): Location this quest giver can be found&#10;">
<polygon fill="none" stroke="black" points="554,-84.5 554,-106.5 678,-106.5 678,-84.5 554,-84.5"/>
<text text-anchor="start" x="581.88" y="-91.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-weight="bold" font-size="14.00">QuestGiver</text>
<polygon fill="none" stroke="black" points="554,-62.5 554,-84.5 603,-84.5 603,-62.5 554,-62.5"/>
<text text-anchor="start" x="563.5" y="-68.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">name</text>
<polygon fill="none" stroke="black" points="603,-62.5 603,-84.5 678,-84.5 678,-62.5 603,-62.5"/>
<text text-anchor="start" x="633.75" y="-68.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">str</text>
<polygon fill="none" stroke="black" points="554,-40.5 554,-62.5 603,-62.5 603,-40.5 554,-40.5"/>
<text text-anchor="start" x="559.75" y="-46.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">faction</text>
<polygon fill="none" stroke="black" points="603,-40.5 603,-62.5 678,-62.5 678,-40.5 603,-40.5"/>
<text text-anchor="start" x="605.62" y="-46.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">Optional[str]</text>
<polygon fill="none" stroke="black" points="554,-18.5 554,-40.5 603,-40.5 603,-18.5 554,-18.5"/>
<text text-anchor="start" x="556.75" y="-24.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">location</text>
<polygon fill="none" stroke="black" points="603,-18.5 603,-40.5 678,-40.5 678,-18.5 603,-18.5"/>
<text text-anchor="start" x="633.75" y="-24.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">str</text>
<polygon fill="none" stroke="black" points="555.75,-86 555.75,-108.5 681,-108.5 681,-86 555.75,-86"/>
<text text-anchor="start" x="584.25" y="-93.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-weight="bold" font-size="14.00">QuestGiver</text>
<polygon fill="none" stroke="black" points="555.75,-63.5 555.75,-86 605.25,-86 605.25,-63.5 555.75,-63.5"/>
<text text-anchor="start" x="565.5" y="-69.7" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">name</text>
<polygon fill="none" stroke="black" points="605.25,-63.5 605.25,-86 681,-86 681,-63.5 605.25,-63.5"/>
<text text-anchor="start" x="636.38" y="-69.7" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">str</text>
<polygon fill="none" stroke="black" points="555.75,-41 555.75,-63.5 605.25,-63.5 605.25,-41 555.75,-41"/>
<text text-anchor="start" x="561.75" y="-47.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">faction</text>
<polygon fill="none" stroke="black" points="605.25,-41 605.25,-63.5 681,-63.5 681,-41 605.25,-41"/>
<text text-anchor="start" x="608.25" y="-47.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">Optional[str]</text>
<polygon fill="none" stroke="black" points="555.75,-18.5 555.75,-41 605.25,-41 605.25,-18.5 555.75,-18.5"/>
<text text-anchor="start" x="558.75" y="-24.7" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">location</text>
<polygon fill="none" stroke="black" points="605.25,-18.5 605.25,-41 681,-41 681,-18.5 605.25,-18.5"/>
<text text-anchor="start" x="636.38" y="-24.7" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">str</text>
</a>

@@ -134,12 +134,12 @@ </g>

<title>erdantic.examples.attrs.Quest:e&#45;&gt;erdantic.examples.attrs.QuestGiver:w</title>
<path fill="none" stroke="black" d="M455.87,-95.5C491.07,-95.5 505.8,-95.5 539.08,-95.5"/>
<polyline fill="none" stroke="black" points="446,-95.5 451,-95.5"/>
<polyline fill="none" stroke="black" points="451,-95.5 456,-95.5"/>
<polyline fill="none" stroke="black" points="554,-95.5 549,-95.5"/>
<polygon fill="black" stroke="black" points="548,-90.5 548,-100.5 546,-100.5 546,-90.5 548,-90.5"/>
<polyline fill="none" stroke="black" points="549,-95.5 544,-95.5"/>
<polygon fill="black" stroke="black" points="543,-90.5 543,-100.5 541,-100.5 541,-90.5 543,-90.5"/>
<polyline fill="none" stroke="black" points="544,-95.5 539,-95.5"/>
<path fill="none" stroke="black" d="M457.62,-97.25C492.82,-97.25 507.55,-97.25 540.83,-97.25"/>
<polyline fill="none" stroke="black" points="447.75,-97.25 452.75,-97.25"/>
<polyline fill="none" stroke="black" points="452.75,-97.25 457.75,-97.25"/>
<polyline fill="none" stroke="black" points="555.75,-97.25 550.75,-97.25"/>
<polygon fill="black" stroke="black" points="549.75,-92.25 549.75,-102.25 547.75,-102.25 547.75,-92.25 549.75,-92.25"/>
<polyline fill="none" stroke="black" points="550.75,-97.25 545.75,-97.25"/>
<polygon fill="black" stroke="black" points="544.75,-92.25 544.75,-102.25 542.75,-102.25 542.75,-92.25 544.75,-92.25"/>
<polyline fill="none" stroke="black" points="545.75,-97.25 540.75,-97.25"/>
</g>
</g>
</svg>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 9.0.0 (0)
<!-- Generated by graphviz version 12.2.1 (0)
-->
<!-- Title: Entity Relationship Diagram created by erdantic Pages: 1 -->
<svg width="686pt" height="309pt"
viewBox="0.00 0.00 686.00 308.50" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 304.5)">
<svg width="689pt" height="314pt"
viewBox="0.00 0.00 689.00 313.75" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 309.75)">
<title>Entity Relationship Diagram created by erdantic</title>
<polygon fill="white" stroke="none" points="-4,4 -4,-304.5 682,-304.5 682,4 -4,4"/>
<text text-anchor="middle" x="339" y="-5.95" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="9.00" fill="#a8a8a8">Created by erdantic vTEST &lt;https://github.com/drivendataorg/erdantic&gt;</text>
<polygon fill="white" stroke="none" points="-4,4 -4,-309.75 685,-309.75 685,4 -4,4"/>
<text text-anchor="middle" x="340.5" y="-5.95" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="9.00" fill="#a8a8a8">Created by erdantic vTEST &lt;https://github.com/drivendataorg/erdantic&gt;</text>
<!-- erdantic.examples.dataclasses.Adventurer -->

@@ -17,20 +17,20 @@ <g id="node1" class="node">

<g id="a_node1"><a xlink:title="erdantic.examples.dataclasses.Adventurer&#10;&#10;A person often late for dinner but with a tale or two to tell.&#10;&#10;Attributes:&#10; &#160;&#160;&#160;name (str): Name of this adventurer&#10; &#160;&#160;&#160;profession (str): Profession of this adventurer&#10; &#160;&#160;&#160;alignment (Alignment): Alignment of this adventurer&#10; &#160;&#160;&#160;level (int): Level of this adventurer&#10;">
<polygon fill="none" stroke="black" points="312,-274.5 312,-296.5 438,-296.5 438,-274.5 312,-274.5"/>
<text text-anchor="start" x="341.25" y="-281.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-weight="bold" font-size="14.00">Adventurer</text>
<polygon fill="none" stroke="black" points="312,-252.5 312,-274.5 374,-274.5 374,-252.5 312,-252.5"/>
<text text-anchor="start" x="328" y="-258.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">name</text>
<polygon fill="none" stroke="black" points="374,-252.5 374,-274.5 438,-274.5 438,-252.5 374,-252.5"/>
<text text-anchor="start" x="399.25" y="-258.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">str</text>
<polygon fill="none" stroke="black" points="312,-230.5 312,-252.5 374,-252.5 374,-230.5 312,-230.5"/>
<text text-anchor="start" x="314.88" y="-236.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">profession</text>
<polygon fill="none" stroke="black" points="374,-230.5 374,-252.5 438,-252.5 438,-230.5 374,-230.5"/>
<text text-anchor="start" x="399.25" y="-236.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">str</text>
<polygon fill="none" stroke="black" points="312,-208.5 312,-230.5 374,-230.5 374,-208.5 312,-208.5"/>
<text text-anchor="start" x="315.62" y="-214.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">alignment</text>
<polygon fill="none" stroke="black" points="374,-208.5 374,-230.5 438,-230.5 438,-208.5 374,-208.5"/>
<text text-anchor="start" x="376.75" y="-214.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">Alignment</text>
<polygon fill="none" stroke="black" points="312,-186.5 312,-208.5 374,-208.5 374,-186.5 312,-186.5"/>
<text text-anchor="start" x="329.88" y="-192.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">level</text>
<polygon fill="none" stroke="black" points="374,-186.5 374,-208.5 438,-208.5 438,-186.5 374,-186.5"/>
<text text-anchor="start" x="398.88" y="-192.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">int</text>
<polygon fill="none" stroke="black" points="313.12,-279.25 313.12,-301.75 439.88,-301.75 439.88,-279.25 313.12,-279.25"/>
<text text-anchor="start" x="342.75" y="-286.45" font-family="Times New Roman,Times,Liberation Serif,serif" font-weight="bold" font-size="14.00">Adventurer</text>
<polygon fill="none" stroke="black" points="313.12,-256.75 313.12,-279.25 375.38,-279.25 375.38,-256.75 313.12,-256.75"/>
<text text-anchor="start" x="329.25" y="-262.95" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">name</text>
<polygon fill="none" stroke="black" points="375.38,-256.75 375.38,-279.25 439.88,-279.25 439.88,-256.75 375.38,-256.75"/>
<text text-anchor="start" x="400.88" y="-262.95" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">str</text>
<polygon fill="none" stroke="black" points="313.12,-234.25 313.12,-256.75 375.38,-256.75 375.38,-234.25 313.12,-234.25"/>
<text text-anchor="start" x="316.12" y="-240.45" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">profession</text>
<polygon fill="none" stroke="black" points="375.38,-234.25 375.38,-256.75 439.88,-256.75 439.88,-234.25 375.38,-234.25"/>
<text text-anchor="start" x="400.88" y="-240.45" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">str</text>
<polygon fill="none" stroke="black" points="313.12,-211.75 313.12,-234.25 375.38,-234.25 375.38,-211.75 313.12,-211.75"/>
<text text-anchor="start" x="316.88" y="-217.95" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">alignment</text>
<polygon fill="none" stroke="black" points="375.38,-211.75 375.38,-234.25 439.88,-234.25 439.88,-211.75 375.38,-211.75"/>
<text text-anchor="start" x="378.38" y="-217.95" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">Alignment</text>
<polygon fill="none" stroke="black" points="313.12,-189.25 313.12,-211.75 375.38,-211.75 375.38,-189.25 313.12,-189.25"/>
<text text-anchor="start" x="331.12" y="-195.45" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">level</text>
<polygon fill="none" stroke="black" points="375.38,-189.25 375.38,-211.75 439.88,-211.75 439.88,-189.25 375.38,-189.25"/>
<text text-anchor="start" x="400.5" y="-195.45" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">int</text>
</a>

@@ -43,20 +43,20 @@ </g>

<g id="a_node2"><a xlink:title="erdantic.examples.dataclasses.Party&#10;&#10;A group of adventurers finding themselves doing and saying things altogether unexpected.&#10;&#10;Attributes:&#10; &#160;&#160;&#160;name (str): Name that party is known by&#10; &#160;&#160;&#160;formed_datetime (datetime): Timestamp of when the party was formed&#10; &#160;&#160;&#160;members (List[Adventurer]): Adventurers that belong to this party&#10; &#160;&#160;&#160;active_quest (Optional[Quest]): Current quest that party is actively tackling&#10;">
<polygon fill="none" stroke="black" points="0,-278.5 0,-300.5 196,-300.5 196,-278.5 0,-278.5"/>
<text text-anchor="start" x="81.88" y="-285.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-weight="bold" font-size="14.00">Party</text>
<polygon fill="none" stroke="black" points="0,-256.5 0,-278.5 99,-278.5 99,-256.5 0,-256.5"/>
<text text-anchor="start" x="34.5" y="-262.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">name</text>
<polygon fill="none" stroke="black" points="99,-256.5 99,-278.5 196,-278.5 196,-256.5 99,-256.5"/>
<text text-anchor="start" x="140.75" y="-262.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">str</text>
<polygon fill="none" stroke="black" points="0,-234.5 0,-256.5 99,-256.5 99,-234.5 0,-234.5"/>
<text text-anchor="start" x="2.62" y="-240.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">formed_datetime</text>
<polygon fill="none" stroke="black" points="99,-234.5 99,-256.5 196,-256.5 196,-234.5 99,-234.5"/>
<text text-anchor="start" x="123.88" y="-240.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">datetime</text>
<polygon fill="none" stroke="black" points="0,-212.5 0,-234.5 99,-234.5 99,-212.5 0,-212.5"/>
<text text-anchor="start" x="24" y="-218.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">members</text>
<polygon fill="none" stroke="black" points="99,-212.5 99,-234.5 196,-234.5 196,-212.5 99,-212.5"/>
<text text-anchor="start" x="101.75" y="-218.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">List[Adventurer]</text>
<polygon fill="none" stroke="black" points="0,-190.5 0,-212.5 99,-212.5 99,-190.5 0,-190.5"/>
<text text-anchor="start" x="15.75" y="-196.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">active_quest</text>
<polygon fill="none" stroke="black" points="99,-190.5 99,-212.5 196,-212.5 196,-190.5 99,-190.5"/>
<text text-anchor="start" x="103.62" y="-196.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">Optional[Quest]</text>
<polygon fill="none" stroke="black" points="0,-283.25 0,-305.75 197.25,-305.75 197.25,-283.25 0,-283.25"/>
<text text-anchor="start" x="82.5" y="-290.45" font-family="Times New Roman,Times,Liberation Serif,serif" font-weight="bold" font-size="14.00">Party</text>
<polygon fill="none" stroke="black" points="0,-260.75 0,-283.25 99.75,-283.25 99.75,-260.75 0,-260.75"/>
<text text-anchor="start" x="34.88" y="-266.95" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">name</text>
<polygon fill="none" stroke="black" points="99.75,-260.75 99.75,-283.25 197.25,-283.25 197.25,-260.75 99.75,-260.75"/>
<text text-anchor="start" x="141.75" y="-266.95" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">str</text>
<polygon fill="none" stroke="black" points="0,-238.25 0,-260.75 99.75,-260.75 99.75,-238.25 0,-238.25"/>
<text text-anchor="start" x="3" y="-244.45" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">formed_datetime</text>
<polygon fill="none" stroke="black" points="99.75,-238.25 99.75,-260.75 197.25,-260.75 197.25,-238.25 99.75,-238.25"/>
<text text-anchor="start" x="124.88" y="-244.45" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">datetime</text>
<polygon fill="none" stroke="black" points="0,-215.75 0,-238.25 99.75,-238.25 99.75,-215.75 0,-215.75"/>
<text text-anchor="start" x="24.38" y="-221.95" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">members</text>
<polygon fill="none" stroke="black" points="99.75,-215.75 99.75,-238.25 197.25,-238.25 197.25,-215.75 99.75,-215.75"/>
<text text-anchor="start" x="102.75" y="-221.95" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">List[Adventurer]</text>
<polygon fill="none" stroke="black" points="0,-193.25 0,-215.75 99.75,-215.75 99.75,-193.25 0,-193.25"/>
<text text-anchor="start" x="16.12" y="-199.45" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">active_quest</text>
<polygon fill="none" stroke="black" points="99.75,-193.25 99.75,-215.75 197.25,-215.75 197.25,-193.25 99.75,-193.25"/>
<text text-anchor="start" x="104.62" y="-199.45" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">Optional[Quest]</text>
</a>

@@ -68,7 +68,7 @@ </g>

<title>erdantic.examples.dataclasses.Party:e&#45;&gt;erdantic.examples.dataclasses.Adventurer:w</title>
<path fill="none" stroke="black" d="M205.92,-224.15C249.25,-230.11 255.51,-274.88 295.89,-283.91"/>
<polyline fill="none" stroke="black" points="196,-223.5 200.99,-223.83"/>
<polyline fill="none" stroke="black" points="200.99,-223.83 205.98,-224.16"/>
<polygon fill="black" stroke="black" points="300.72,-284.42 310.2,-289.94 306.03,-284.98 310.33,-285.43 310.33,-285.43 310.33,-285.43 306.03,-284.98 311.14,-280.99 300.72,-284.42"/>
<polyline fill="none" stroke="black" points="299.51,-284.29 294.54,-283.77"/>
<path fill="none" stroke="black" d="M207.21,-227.67C250.68,-233.77 256.49,-279.62 296.96,-288.88"/>
<polyline fill="none" stroke="black" points="197.25,-227 202.24,-227.34"/>
<polyline fill="none" stroke="black" points="202.24,-227.34 207.23,-227.67"/>
<polygon fill="black" stroke="black" points="301.85,-289.4 311.31,-294.94 307.15,-289.97 311.46,-290.43 311.46,-290.43 311.46,-290.43 307.15,-289.97 312.27,-285.99 301.85,-289.4"/>
<polyline fill="none" stroke="black" points="300.64,-289.27 295.67,-288.74"/>
</g>

@@ -79,16 +79,16 @@ <!-- erdantic.examples.dataclasses.Quest -->

<g id="a_node3"><a xlink:title="erdantic.examples.dataclasses.Quest&#10;&#10;A task to complete, with some monetary reward.&#10;&#10;Attributes:&#10; &#160;&#160;&#160;name (str): Name by which this quest is referred to&#10; &#160;&#160;&#160;giver (QuestGiver): Person who offered the quest&#10; &#160;&#160;&#160;reward_gold (int): Amount of gold to be rewarded for quest completion&#10;">
<polygon fill="none" stroke="black" points="304,-128.5 304,-150.5 446,-150.5 446,-128.5 304,-128.5"/>
<text text-anchor="start" x="357.75" y="-135.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-weight="bold" font-size="14.00">Quest</text>
<polygon fill="none" stroke="black" points="304,-106.5 304,-128.5 378,-128.5 378,-106.5 304,-106.5"/>
<text text-anchor="start" x="326" y="-112.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">name</text>
<polygon fill="none" stroke="black" points="378,-106.5 378,-128.5 446,-128.5 446,-106.5 378,-106.5"/>
<text text-anchor="start" x="405.25" y="-112.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">str</text>
<polygon fill="none" stroke="black" points="304,-84.5 304,-106.5 378,-106.5 378,-84.5 304,-84.5"/>
<text text-anchor="start" x="327.12" y="-90.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">giver</text>
<polygon fill="none" stroke="black" points="378,-84.5 378,-106.5 446,-106.5 446,-84.5 378,-84.5"/>
<text text-anchor="start" x="380.88" y="-90.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">QuestGiver</text>
<polygon fill="none" stroke="black" points="304,-62.5 304,-84.5 378,-84.5 378,-62.5 304,-62.5"/>
<text text-anchor="start" x="306.88" y="-68.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">reward_gold</text>
<polygon fill="none" stroke="black" points="378,-62.5 378,-84.5 446,-84.5 446,-62.5 378,-62.5"/>
<text text-anchor="start" x="404.88" y="-68.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">int</text>
<polygon fill="none" stroke="black" points="305.25,-131 305.25,-153.5 447.75,-153.5 447.75,-131 305.25,-131"/>
<text text-anchor="start" x="359.25" y="-138.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-weight="bold" font-size="14.00">Quest</text>
<polygon fill="none" stroke="black" points="305.25,-108.5 305.25,-131 379.5,-131 379.5,-108.5 305.25,-108.5"/>
<text text-anchor="start" x="327.38" y="-114.7" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">name</text>
<polygon fill="none" stroke="black" points="379.5,-108.5 379.5,-131 447.75,-131 447.75,-108.5 379.5,-108.5"/>
<text text-anchor="start" x="406.88" y="-114.7" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">str</text>
<polygon fill="none" stroke="black" points="305.25,-86 305.25,-108.5 379.5,-108.5 379.5,-86 305.25,-86"/>
<text text-anchor="start" x="328.5" y="-92.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">giver</text>
<polygon fill="none" stroke="black" points="379.5,-86 379.5,-108.5 447.75,-108.5 447.75,-86 379.5,-86"/>
<text text-anchor="start" x="382.5" y="-92.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">QuestGiver</text>
<polygon fill="none" stroke="black" points="305.25,-63.5 305.25,-86 379.5,-86 379.5,-63.5 305.25,-63.5"/>
<text text-anchor="start" x="308.25" y="-69.7" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">reward_gold</text>
<polygon fill="none" stroke="black" points="379.5,-63.5 379.5,-86 447.75,-86 447.75,-63.5 379.5,-63.5"/>
<text text-anchor="start" x="406.5" y="-69.7" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">int</text>
</a>

@@ -100,9 +100,9 @@ </g>

<title>erdantic.examples.dataclasses.Party:e&#45;&gt;erdantic.examples.dataclasses.Quest:w</title>
<path fill="none" stroke="black" d="M205.73,-200.8C245.01,-194.92 251.04,-153.51 285.33,-142.29"/>
<polyline fill="none" stroke="black" points="196,-201.5 200.99,-201.14"/>
<polyline fill="none" stroke="black" points="200.99,-201.14 205.97,-200.79"/>
<polyline fill="none" stroke="black" points="304,-139.5 299.06,-140.24"/>
<polygon fill="black" stroke="black" points="297.33,-135.44 298.81,-145.33 296.83,-145.63 295.35,-135.74 297.33,-135.44"/>
<polyline fill="none" stroke="black" points="299.06,-140.24 294.11,-140.98"/>
<ellipse fill="none" stroke="black" cx="289.66" cy="-141.65" rx="4" ry="4"/>
<path fill="none" stroke="black" d="M206.98,-203.8C246.3,-197.9 252.26,-156.32 286.57,-145.06"/>
<polyline fill="none" stroke="black" points="197.25,-204.5 202.24,-204.14"/>
<polyline fill="none" stroke="black" points="202.24,-204.14 207.22,-203.78"/>
<polyline fill="none" stroke="black" points="305.25,-142.25 300.31,-142.99"/>
<polygon fill="black" stroke="black" points="298.57,-138.2 300.06,-148.09 298.08,-148.38 296.6,-138.49 298.57,-138.2"/>
<polyline fill="none" stroke="black" points="300.31,-142.99 295.36,-143.74"/>
<ellipse fill="none" stroke="black" cx="290.91" cy="-144.4" rx="4" ry="4"/>
</g>

@@ -113,16 +113,16 @@ <!-- erdantic.examples.dataclasses.QuestGiver -->

<g id="a_node4"><a xlink:title="erdantic.examples.dataclasses.QuestGiver&#10;&#10;A person who offers a task that needs completing.&#10;&#10;Attributes:&#10; &#160;&#160;&#160;name (str): Name of this quest giver&#10; &#160;&#160;&#160;faction (str): Faction that this quest giver belongs to&#10; &#160;&#160;&#160;location (str): Location this quest giver can be found&#10;">
<polygon fill="none" stroke="black" points="554,-84.5 554,-106.5 678,-106.5 678,-84.5 554,-84.5"/>
<text text-anchor="start" x="581.88" y="-91.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-weight="bold" font-size="14.00">QuestGiver</text>
<polygon fill="none" stroke="black" points="554,-62.5 554,-84.5 603,-84.5 603,-62.5 554,-62.5"/>
<text text-anchor="start" x="563.5" y="-68.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">name</text>
<polygon fill="none" stroke="black" points="603,-62.5 603,-84.5 678,-84.5 678,-62.5 603,-62.5"/>
<text text-anchor="start" x="633.75" y="-68.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">str</text>
<polygon fill="none" stroke="black" points="554,-40.5 554,-62.5 603,-62.5 603,-40.5 554,-40.5"/>
<text text-anchor="start" x="559.75" y="-46.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">faction</text>
<polygon fill="none" stroke="black" points="603,-40.5 603,-62.5 678,-62.5 678,-40.5 603,-40.5"/>
<text text-anchor="start" x="605.62" y="-46.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">Optional[str]</text>
<polygon fill="none" stroke="black" points="554,-18.5 554,-40.5 603,-40.5 603,-18.5 554,-18.5"/>
<text text-anchor="start" x="556.75" y="-24.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">location</text>
<polygon fill="none" stroke="black" points="603,-18.5 603,-40.5 678,-40.5 678,-18.5 603,-18.5"/>
<text text-anchor="start" x="633.75" y="-24.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">str</text>
<polygon fill="none" stroke="black" points="555.75,-86 555.75,-108.5 681,-108.5 681,-86 555.75,-86"/>
<text text-anchor="start" x="584.25" y="-93.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-weight="bold" font-size="14.00">QuestGiver</text>
<polygon fill="none" stroke="black" points="555.75,-63.5 555.75,-86 605.25,-86 605.25,-63.5 555.75,-63.5"/>
<text text-anchor="start" x="565.5" y="-69.7" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">name</text>
<polygon fill="none" stroke="black" points="605.25,-63.5 605.25,-86 681,-86 681,-63.5 605.25,-63.5"/>
<text text-anchor="start" x="636.38" y="-69.7" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">str</text>
<polygon fill="none" stroke="black" points="555.75,-41 555.75,-63.5 605.25,-63.5 605.25,-41 555.75,-41"/>
<text text-anchor="start" x="561.75" y="-47.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">faction</text>
<polygon fill="none" stroke="black" points="605.25,-41 605.25,-63.5 681,-63.5 681,-41 605.25,-41"/>
<text text-anchor="start" x="608.25" y="-47.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">Optional[str]</text>
<polygon fill="none" stroke="black" points="555.75,-18.5 555.75,-41 605.25,-41 605.25,-18.5 555.75,-18.5"/>
<text text-anchor="start" x="558.75" y="-24.7" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">location</text>
<polygon fill="none" stroke="black" points="605.25,-18.5 605.25,-41 681,-41 681,-18.5 605.25,-18.5"/>
<text text-anchor="start" x="636.38" y="-24.7" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">str</text>
</a>

@@ -134,12 +134,12 @@ </g>

<title>erdantic.examples.dataclasses.Quest:e&#45;&gt;erdantic.examples.dataclasses.QuestGiver:w</title>
<path fill="none" stroke="black" d="M455.87,-95.5C491.07,-95.5 505.8,-95.5 539.08,-95.5"/>
<polyline fill="none" stroke="black" points="446,-95.5 451,-95.5"/>
<polyline fill="none" stroke="black" points="451,-95.5 456,-95.5"/>
<polyline fill="none" stroke="black" points="554,-95.5 549,-95.5"/>
<polygon fill="black" stroke="black" points="548,-90.5 548,-100.5 546,-100.5 546,-90.5 548,-90.5"/>
<polyline fill="none" stroke="black" points="549,-95.5 544,-95.5"/>
<polygon fill="black" stroke="black" points="543,-90.5 543,-100.5 541,-100.5 541,-90.5 543,-90.5"/>
<polyline fill="none" stroke="black" points="544,-95.5 539,-95.5"/>
<path fill="none" stroke="black" d="M457.62,-97.25C492.82,-97.25 507.55,-97.25 540.83,-97.25"/>
<polyline fill="none" stroke="black" points="447.75,-97.25 452.75,-97.25"/>
<polyline fill="none" stroke="black" points="452.75,-97.25 457.75,-97.25"/>
<polyline fill="none" stroke="black" points="555.75,-97.25 550.75,-97.25"/>
<polygon fill="black" stroke="black" points="549.75,-92.25 549.75,-102.25 547.75,-102.25 547.75,-92.25 549.75,-92.25"/>
<polyline fill="none" stroke="black" points="550.75,-97.25 545.75,-97.25"/>
<polygon fill="black" stroke="black" points="544.75,-92.25 544.75,-102.25 542.75,-102.25 542.75,-92.25 544.75,-92.25"/>
<polyline fill="none" stroke="black" points="545.75,-97.25 540.75,-97.25"/>
</g>
</g>
</svg>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 9.0.0 (0)
<!-- Generated by graphviz version 12.2.1 (0)
-->
<!-- Title: Entity Relationship Diagram created by erdantic Pages: 1 -->
<svg width="686pt" height="309pt"
viewBox="0.00 0.00 686.00 308.50" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 304.5)">
<svg width="689pt" height="314pt"
viewBox="0.00 0.00 689.00 313.75" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 309.75)">
<title>Entity Relationship Diagram created by erdantic</title>
<polygon fill="white" stroke="none" points="-4,4 -4,-304.5 682,-304.5 682,4 -4,4"/>
<text text-anchor="middle" x="339" y="-5.95" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="9.00" fill="#a8a8a8">Created by erdantic vTEST &lt;https://github.com/drivendataorg/erdantic&gt;</text>
<polygon fill="white" stroke="none" points="-4,4 -4,-309.75 685,-309.75 685,4 -4,4"/>
<text text-anchor="middle" x="340.5" y="-5.95" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="9.00" fill="#a8a8a8">Created by erdantic vTEST &lt;https://github.com/drivendataorg/erdantic&gt;</text>
<!-- erdantic.examples.pydantic_v1.Adventurer -->

@@ -17,20 +17,20 @@ <g id="node1" class="node">

<g id="a_node1"><a xlink:title="erdantic.examples.pydantic_v1.Adventurer&#10;&#10;A person often late for dinner but with a tale or two to tell.&#10;&#10;Attributes:&#10; &#160;&#160;&#160;name (str): Name of this adventurer&#10; &#160;&#160;&#160;profession (str): Profession of this adventurer&#10; &#160;&#160;&#160;alignment (Alignment): Alignment of this adventurer&#10; &#160;&#160;&#160;level (int): Level of this adventurer&#10;">
<polygon fill="none" stroke="black" points="312,-274.5 312,-296.5 438,-296.5 438,-274.5 312,-274.5"/>
<text text-anchor="start" x="341.25" y="-281.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-weight="bold" font-size="14.00">Adventurer</text>
<polygon fill="none" stroke="black" points="312,-252.5 312,-274.5 374,-274.5 374,-252.5 312,-252.5"/>
<text text-anchor="start" x="328" y="-258.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">name</text>
<polygon fill="none" stroke="black" points="374,-252.5 374,-274.5 438,-274.5 438,-252.5 374,-252.5"/>
<text text-anchor="start" x="399.25" y="-258.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">str</text>
<polygon fill="none" stroke="black" points="312,-230.5 312,-252.5 374,-252.5 374,-230.5 312,-230.5"/>
<text text-anchor="start" x="314.88" y="-236.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">profession</text>
<polygon fill="none" stroke="black" points="374,-230.5 374,-252.5 438,-252.5 438,-230.5 374,-230.5"/>
<text text-anchor="start" x="399.25" y="-236.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">str</text>
<polygon fill="none" stroke="black" points="312,-208.5 312,-230.5 374,-230.5 374,-208.5 312,-208.5"/>
<text text-anchor="start" x="315.62" y="-214.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">alignment</text>
<polygon fill="none" stroke="black" points="374,-208.5 374,-230.5 438,-230.5 438,-208.5 374,-208.5"/>
<text text-anchor="start" x="376.75" y="-214.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">Alignment</text>
<polygon fill="none" stroke="black" points="312,-186.5 312,-208.5 374,-208.5 374,-186.5 312,-186.5"/>
<text text-anchor="start" x="329.88" y="-192.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">level</text>
<polygon fill="none" stroke="black" points="374,-186.5 374,-208.5 438,-208.5 438,-186.5 374,-186.5"/>
<text text-anchor="start" x="398.88" y="-192.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">int</text>
<polygon fill="none" stroke="black" points="313.12,-279.25 313.12,-301.75 439.88,-301.75 439.88,-279.25 313.12,-279.25"/>
<text text-anchor="start" x="342.75" y="-286.45" font-family="Times New Roman,Times,Liberation Serif,serif" font-weight="bold" font-size="14.00">Adventurer</text>
<polygon fill="none" stroke="black" points="313.12,-256.75 313.12,-279.25 375.38,-279.25 375.38,-256.75 313.12,-256.75"/>
<text text-anchor="start" x="329.25" y="-262.95" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">name</text>
<polygon fill="none" stroke="black" points="375.38,-256.75 375.38,-279.25 439.88,-279.25 439.88,-256.75 375.38,-256.75"/>
<text text-anchor="start" x="400.88" y="-262.95" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">str</text>
<polygon fill="none" stroke="black" points="313.12,-234.25 313.12,-256.75 375.38,-256.75 375.38,-234.25 313.12,-234.25"/>
<text text-anchor="start" x="316.12" y="-240.45" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">profession</text>
<polygon fill="none" stroke="black" points="375.38,-234.25 375.38,-256.75 439.88,-256.75 439.88,-234.25 375.38,-234.25"/>
<text text-anchor="start" x="400.88" y="-240.45" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">str</text>
<polygon fill="none" stroke="black" points="313.12,-211.75 313.12,-234.25 375.38,-234.25 375.38,-211.75 313.12,-211.75"/>
<text text-anchor="start" x="316.88" y="-217.95" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">alignment</text>
<polygon fill="none" stroke="black" points="375.38,-211.75 375.38,-234.25 439.88,-234.25 439.88,-211.75 375.38,-211.75"/>
<text text-anchor="start" x="378.38" y="-217.95" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">Alignment</text>
<polygon fill="none" stroke="black" points="313.12,-189.25 313.12,-211.75 375.38,-211.75 375.38,-189.25 313.12,-189.25"/>
<text text-anchor="start" x="331.12" y="-195.45" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">level</text>
<polygon fill="none" stroke="black" points="375.38,-189.25 375.38,-211.75 439.88,-211.75 439.88,-189.25 375.38,-189.25"/>
<text text-anchor="start" x="400.5" y="-195.45" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">int</text>
</a>

@@ -43,20 +43,20 @@ </g>

<g id="a_node2"><a xlink:title="erdantic.examples.pydantic_v1.Party&#10;&#10;A group of adventurers finding themselves doing and saying things altogether unexpected.&#10;&#10;Attributes:&#10; &#160;&#160;&#160;name (str): Name that party is known by&#10; &#160;&#160;&#160;formed_datetime (datetime): Timestamp of when the party was formed&#10; &#160;&#160;&#160;members (List[Adventurer]): Adventurers that belong to this party&#10; &#160;&#160;&#160;active_quest (Optional[Quest]): Current quest that party is actively tackling&#10;">
<polygon fill="none" stroke="black" points="0,-278.5 0,-300.5 196,-300.5 196,-278.5 0,-278.5"/>
<text text-anchor="start" x="81.88" y="-285.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-weight="bold" font-size="14.00">Party</text>
<polygon fill="none" stroke="black" points="0,-256.5 0,-278.5 99,-278.5 99,-256.5 0,-256.5"/>
<text text-anchor="start" x="34.5" y="-262.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">name</text>
<polygon fill="none" stroke="black" points="99,-256.5 99,-278.5 196,-278.5 196,-256.5 99,-256.5"/>
<text text-anchor="start" x="140.75" y="-262.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">str</text>
<polygon fill="none" stroke="black" points="0,-234.5 0,-256.5 99,-256.5 99,-234.5 0,-234.5"/>
<text text-anchor="start" x="2.62" y="-240.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">formed_datetime</text>
<polygon fill="none" stroke="black" points="99,-234.5 99,-256.5 196,-256.5 196,-234.5 99,-234.5"/>
<text text-anchor="start" x="123.88" y="-240.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">datetime</text>
<polygon fill="none" stroke="black" points="0,-212.5 0,-234.5 99,-234.5 99,-212.5 0,-212.5"/>
<text text-anchor="start" x="24" y="-218.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">members</text>
<polygon fill="none" stroke="black" points="99,-212.5 99,-234.5 196,-234.5 196,-212.5 99,-212.5"/>
<text text-anchor="start" x="101.75" y="-218.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">List[Adventurer]</text>
<polygon fill="none" stroke="black" points="0,-190.5 0,-212.5 99,-212.5 99,-190.5 0,-190.5"/>
<text text-anchor="start" x="15.75" y="-196.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">active_quest</text>
<polygon fill="none" stroke="black" points="99,-190.5 99,-212.5 196,-212.5 196,-190.5 99,-190.5"/>
<text text-anchor="start" x="103.62" y="-196.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">Optional[Quest]</text>
<polygon fill="none" stroke="black" points="0,-283.25 0,-305.75 197.25,-305.75 197.25,-283.25 0,-283.25"/>
<text text-anchor="start" x="82.5" y="-290.45" font-family="Times New Roman,Times,Liberation Serif,serif" font-weight="bold" font-size="14.00">Party</text>
<polygon fill="none" stroke="black" points="0,-260.75 0,-283.25 99.75,-283.25 99.75,-260.75 0,-260.75"/>
<text text-anchor="start" x="34.88" y="-266.95" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">name</text>
<polygon fill="none" stroke="black" points="99.75,-260.75 99.75,-283.25 197.25,-283.25 197.25,-260.75 99.75,-260.75"/>
<text text-anchor="start" x="141.75" y="-266.95" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">str</text>
<polygon fill="none" stroke="black" points="0,-238.25 0,-260.75 99.75,-260.75 99.75,-238.25 0,-238.25"/>
<text text-anchor="start" x="3" y="-244.45" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">formed_datetime</text>
<polygon fill="none" stroke="black" points="99.75,-238.25 99.75,-260.75 197.25,-260.75 197.25,-238.25 99.75,-238.25"/>
<text text-anchor="start" x="124.88" y="-244.45" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">datetime</text>
<polygon fill="none" stroke="black" points="0,-215.75 0,-238.25 99.75,-238.25 99.75,-215.75 0,-215.75"/>
<text text-anchor="start" x="24.38" y="-221.95" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">members</text>
<polygon fill="none" stroke="black" points="99.75,-215.75 99.75,-238.25 197.25,-238.25 197.25,-215.75 99.75,-215.75"/>
<text text-anchor="start" x="102.75" y="-221.95" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">List[Adventurer]</text>
<polygon fill="none" stroke="black" points="0,-193.25 0,-215.75 99.75,-215.75 99.75,-193.25 0,-193.25"/>
<text text-anchor="start" x="16.12" y="-199.45" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">active_quest</text>
<polygon fill="none" stroke="black" points="99.75,-193.25 99.75,-215.75 197.25,-215.75 197.25,-193.25 99.75,-193.25"/>
<text text-anchor="start" x="104.62" y="-199.45" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">Optional[Quest]</text>
</a>

@@ -68,7 +68,7 @@ </g>

<title>erdantic.examples.pydantic_v1.Party:e&#45;&gt;erdantic.examples.pydantic_v1.Adventurer:w</title>
<path fill="none" stroke="black" d="M205.92,-224.15C249.25,-230.11 255.51,-274.88 295.89,-283.91"/>
<polyline fill="none" stroke="black" points="196,-223.5 200.99,-223.83"/>
<polyline fill="none" stroke="black" points="200.99,-223.83 205.98,-224.16"/>
<polygon fill="black" stroke="black" points="300.72,-284.42 310.2,-289.94 306.03,-284.98 310.33,-285.43 310.33,-285.43 310.33,-285.43 306.03,-284.98 311.14,-280.99 300.72,-284.42"/>
<polyline fill="none" stroke="black" points="299.51,-284.29 294.54,-283.77"/>
<path fill="none" stroke="black" d="M207.21,-227.67C250.68,-233.77 256.49,-279.62 296.96,-288.88"/>
<polyline fill="none" stroke="black" points="197.25,-227 202.24,-227.34"/>
<polyline fill="none" stroke="black" points="202.24,-227.34 207.23,-227.67"/>
<polygon fill="black" stroke="black" points="301.85,-289.4 311.31,-294.94 307.15,-289.97 311.46,-290.43 311.46,-290.43 311.46,-290.43 307.15,-289.97 312.27,-285.99 301.85,-289.4"/>
<polyline fill="none" stroke="black" points="300.64,-289.27 295.67,-288.74"/>
</g>

@@ -79,16 +79,16 @@ <!-- erdantic.examples.pydantic_v1.Quest -->

<g id="a_node3"><a xlink:title="erdantic.examples.pydantic_v1.Quest&#10;&#10;A task to complete, with some monetary reward.&#10;&#10;Attributes:&#10; &#160;&#160;&#160;name (str): Name by which this quest is referred to&#10; &#160;&#160;&#160;giver (QuestGiver): Person who offered the quest&#10; &#160;&#160;&#160;reward_gold (int): Amount of gold to be rewarded for quest completion&#10;">
<polygon fill="none" stroke="black" points="304,-128.5 304,-150.5 446,-150.5 446,-128.5 304,-128.5"/>
<text text-anchor="start" x="357.75" y="-135.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-weight="bold" font-size="14.00">Quest</text>
<polygon fill="none" stroke="black" points="304,-106.5 304,-128.5 378,-128.5 378,-106.5 304,-106.5"/>
<text text-anchor="start" x="326" y="-112.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">name</text>
<polygon fill="none" stroke="black" points="378,-106.5 378,-128.5 446,-128.5 446,-106.5 378,-106.5"/>
<text text-anchor="start" x="405.25" y="-112.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">str</text>
<polygon fill="none" stroke="black" points="304,-84.5 304,-106.5 378,-106.5 378,-84.5 304,-84.5"/>
<text text-anchor="start" x="327.12" y="-90.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">giver</text>
<polygon fill="none" stroke="black" points="378,-84.5 378,-106.5 446,-106.5 446,-84.5 378,-84.5"/>
<text text-anchor="start" x="380.88" y="-90.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">QuestGiver</text>
<polygon fill="none" stroke="black" points="304,-62.5 304,-84.5 378,-84.5 378,-62.5 304,-62.5"/>
<text text-anchor="start" x="306.88" y="-68.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">reward_gold</text>
<polygon fill="none" stroke="black" points="378,-62.5 378,-84.5 446,-84.5 446,-62.5 378,-62.5"/>
<text text-anchor="start" x="404.88" y="-68.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">int</text>
<polygon fill="none" stroke="black" points="305.25,-131 305.25,-153.5 447.75,-153.5 447.75,-131 305.25,-131"/>
<text text-anchor="start" x="359.25" y="-138.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-weight="bold" font-size="14.00">Quest</text>
<polygon fill="none" stroke="black" points="305.25,-108.5 305.25,-131 379.5,-131 379.5,-108.5 305.25,-108.5"/>
<text text-anchor="start" x="327.38" y="-114.7" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">name</text>
<polygon fill="none" stroke="black" points="379.5,-108.5 379.5,-131 447.75,-131 447.75,-108.5 379.5,-108.5"/>
<text text-anchor="start" x="406.88" y="-114.7" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">str</text>
<polygon fill="none" stroke="black" points="305.25,-86 305.25,-108.5 379.5,-108.5 379.5,-86 305.25,-86"/>
<text text-anchor="start" x="328.5" y="-92.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">giver</text>
<polygon fill="none" stroke="black" points="379.5,-86 379.5,-108.5 447.75,-108.5 447.75,-86 379.5,-86"/>
<text text-anchor="start" x="382.5" y="-92.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">QuestGiver</text>
<polygon fill="none" stroke="black" points="305.25,-63.5 305.25,-86 379.5,-86 379.5,-63.5 305.25,-63.5"/>
<text text-anchor="start" x="308.25" y="-69.7" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">reward_gold</text>
<polygon fill="none" stroke="black" points="379.5,-63.5 379.5,-86 447.75,-86 447.75,-63.5 379.5,-63.5"/>
<text text-anchor="start" x="406.5" y="-69.7" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">int</text>
</a>

@@ -100,9 +100,9 @@ </g>

<title>erdantic.examples.pydantic_v1.Party:e&#45;&gt;erdantic.examples.pydantic_v1.Quest:w</title>
<path fill="none" stroke="black" d="M205.73,-200.8C245.01,-194.92 251.04,-153.51 285.33,-142.29"/>
<polyline fill="none" stroke="black" points="196,-201.5 200.99,-201.14"/>
<polyline fill="none" stroke="black" points="200.99,-201.14 205.97,-200.79"/>
<polyline fill="none" stroke="black" points="304,-139.5 299.06,-140.24"/>
<polygon fill="black" stroke="black" points="297.33,-135.44 298.81,-145.33 296.83,-145.63 295.35,-135.74 297.33,-135.44"/>
<polyline fill="none" stroke="black" points="299.06,-140.24 294.11,-140.98"/>
<ellipse fill="none" stroke="black" cx="289.66" cy="-141.65" rx="4" ry="4"/>
<path fill="none" stroke="black" d="M206.98,-203.8C246.3,-197.9 252.26,-156.32 286.57,-145.06"/>
<polyline fill="none" stroke="black" points="197.25,-204.5 202.24,-204.14"/>
<polyline fill="none" stroke="black" points="202.24,-204.14 207.22,-203.78"/>
<polyline fill="none" stroke="black" points="305.25,-142.25 300.31,-142.99"/>
<polygon fill="black" stroke="black" points="298.57,-138.2 300.06,-148.09 298.08,-148.38 296.6,-138.49 298.57,-138.2"/>
<polyline fill="none" stroke="black" points="300.31,-142.99 295.36,-143.74"/>
<ellipse fill="none" stroke="black" cx="290.91" cy="-144.4" rx="4" ry="4"/>
</g>

@@ -113,16 +113,16 @@ <!-- erdantic.examples.pydantic_v1.QuestGiver -->

<g id="a_node4"><a xlink:title="erdantic.examples.pydantic_v1.QuestGiver&#10;&#10;A person who offers a task that needs completing.&#10;&#10;Attributes:&#10; &#160;&#160;&#160;name (str): Name of this quest giver&#10; &#160;&#160;&#160;faction (str): Faction that this quest giver belongs to&#10; &#160;&#160;&#160;location (str): Location this quest giver can be found&#10;">
<polygon fill="none" stroke="black" points="554,-84.5 554,-106.5 678,-106.5 678,-84.5 554,-84.5"/>
<text text-anchor="start" x="581.88" y="-91.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-weight="bold" font-size="14.00">QuestGiver</text>
<polygon fill="none" stroke="black" points="554,-62.5 554,-84.5 603,-84.5 603,-62.5 554,-62.5"/>
<text text-anchor="start" x="563.5" y="-68.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">name</text>
<polygon fill="none" stroke="black" points="603,-62.5 603,-84.5 678,-84.5 678,-62.5 603,-62.5"/>
<text text-anchor="start" x="633.75" y="-68.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">str</text>
<polygon fill="none" stroke="black" points="554,-40.5 554,-62.5 603,-62.5 603,-40.5 554,-40.5"/>
<text text-anchor="start" x="559.75" y="-46.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">faction</text>
<polygon fill="none" stroke="black" points="603,-40.5 603,-62.5 678,-62.5 678,-40.5 603,-40.5"/>
<text text-anchor="start" x="605.62" y="-46.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">Optional[str]</text>
<polygon fill="none" stroke="black" points="554,-18.5 554,-40.5 603,-40.5 603,-18.5 554,-18.5"/>
<text text-anchor="start" x="556.75" y="-24.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">location</text>
<polygon fill="none" stroke="black" points="603,-18.5 603,-40.5 678,-40.5 678,-18.5 603,-18.5"/>
<text text-anchor="start" x="633.75" y="-24.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">str</text>
<polygon fill="none" stroke="black" points="555.75,-86 555.75,-108.5 681,-108.5 681,-86 555.75,-86"/>
<text text-anchor="start" x="584.25" y="-93.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-weight="bold" font-size="14.00">QuestGiver</text>
<polygon fill="none" stroke="black" points="555.75,-63.5 555.75,-86 605.25,-86 605.25,-63.5 555.75,-63.5"/>
<text text-anchor="start" x="565.5" y="-69.7" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">name</text>
<polygon fill="none" stroke="black" points="605.25,-63.5 605.25,-86 681,-86 681,-63.5 605.25,-63.5"/>
<text text-anchor="start" x="636.38" y="-69.7" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">str</text>
<polygon fill="none" stroke="black" points="555.75,-41 555.75,-63.5 605.25,-63.5 605.25,-41 555.75,-41"/>
<text text-anchor="start" x="561.75" y="-47.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">faction</text>
<polygon fill="none" stroke="black" points="605.25,-41 605.25,-63.5 681,-63.5 681,-41 605.25,-41"/>
<text text-anchor="start" x="608.25" y="-47.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">Optional[str]</text>
<polygon fill="none" stroke="black" points="555.75,-18.5 555.75,-41 605.25,-41 605.25,-18.5 555.75,-18.5"/>
<text text-anchor="start" x="558.75" y="-24.7" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">location</text>
<polygon fill="none" stroke="black" points="605.25,-18.5 605.25,-41 681,-41 681,-18.5 605.25,-18.5"/>
<text text-anchor="start" x="636.38" y="-24.7" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">str</text>
</a>

@@ -134,12 +134,12 @@ </g>

<title>erdantic.examples.pydantic_v1.Quest:e&#45;&gt;erdantic.examples.pydantic_v1.QuestGiver:w</title>
<path fill="none" stroke="black" d="M455.87,-95.5C491.07,-95.5 505.8,-95.5 539.08,-95.5"/>
<polyline fill="none" stroke="black" points="446,-95.5 451,-95.5"/>
<polyline fill="none" stroke="black" points="451,-95.5 456,-95.5"/>
<polyline fill="none" stroke="black" points="554,-95.5 549,-95.5"/>
<polygon fill="black" stroke="black" points="548,-90.5 548,-100.5 546,-100.5 546,-90.5 548,-90.5"/>
<polyline fill="none" stroke="black" points="549,-95.5 544,-95.5"/>
<polygon fill="black" stroke="black" points="543,-90.5 543,-100.5 541,-100.5 541,-90.5 543,-90.5"/>
<polyline fill="none" stroke="black" points="544,-95.5 539,-95.5"/>
<path fill="none" stroke="black" d="M457.62,-97.25C492.82,-97.25 507.55,-97.25 540.83,-97.25"/>
<polyline fill="none" stroke="black" points="447.75,-97.25 452.75,-97.25"/>
<polyline fill="none" stroke="black" points="452.75,-97.25 457.75,-97.25"/>
<polyline fill="none" stroke="black" points="555.75,-97.25 550.75,-97.25"/>
<polygon fill="black" stroke="black" points="549.75,-92.25 549.75,-102.25 547.75,-102.25 547.75,-92.25 549.75,-92.25"/>
<polyline fill="none" stroke="black" points="550.75,-97.25 545.75,-97.25"/>
<polygon fill="black" stroke="black" points="544.75,-92.25 544.75,-102.25 542.75,-102.25 542.75,-92.25 544.75,-92.25"/>
<polyline fill="none" stroke="black" points="545.75,-97.25 540.75,-97.25"/>
</g>
</g>
</svg>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 9.0.0 (0)
<!-- Generated by graphviz version 12.2.1 (0)
-->
<!-- Title: Entity Relationship Diagram created by erdantic Pages: 1 -->
<svg width="686pt" height="309pt"
viewBox="0.00 0.00 686.00 308.50" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 304.5)">
<svg width="689pt" height="314pt"
viewBox="0.00 0.00 689.00 313.75" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 309.75)">
<title>Entity Relationship Diagram created by erdantic</title>
<polygon fill="white" stroke="none" points="-4,4 -4,-304.5 682,-304.5 682,4 -4,4"/>
<text text-anchor="middle" x="339" y="-5.95" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="9.00" fill="#a8a8a8">Created by erdantic vTEST &lt;https://github.com/drivendataorg/erdantic&gt;</text>
<polygon fill="white" stroke="none" points="-4,4 -4,-309.75 685,-309.75 685,4 -4,4"/>
<text text-anchor="middle" x="340.5" y="-5.95" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="9.00" fill="#a8a8a8">Created by erdantic vTEST &lt;https://github.com/drivendataorg/erdantic&gt;</text>
<!-- erdantic.examples.pydantic.Adventurer -->

@@ -17,20 +17,20 @@ <g id="node1" class="node">

<g id="a_node1"><a xlink:title="erdantic.examples.pydantic.Adventurer&#10;&#10;A person often late for dinner but with a tale or two to tell.&#10;&#10;Attributes:&#10; &#160;&#160;&#160;name (str): Name of this adventurer&#10; &#160;&#160;&#160;profession (str): Profession of this adventurer&#10; &#160;&#160;&#160;alignment (Alignment): Alignment of this adventurer&#10; &#160;&#160;&#160;level (int): Level of this adventurer&#10;">
<polygon fill="none" stroke="black" points="312,-274.5 312,-296.5 438,-296.5 438,-274.5 312,-274.5"/>
<text text-anchor="start" x="341.25" y="-281.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-weight="bold" font-size="14.00">Adventurer</text>
<polygon fill="none" stroke="black" points="312,-252.5 312,-274.5 374,-274.5 374,-252.5 312,-252.5"/>
<text text-anchor="start" x="328" y="-258.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">name</text>
<polygon fill="none" stroke="black" points="374,-252.5 374,-274.5 438,-274.5 438,-252.5 374,-252.5"/>
<text text-anchor="start" x="399.25" y="-258.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">str</text>
<polygon fill="none" stroke="black" points="312,-230.5 312,-252.5 374,-252.5 374,-230.5 312,-230.5"/>
<text text-anchor="start" x="314.88" y="-236.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">profession</text>
<polygon fill="none" stroke="black" points="374,-230.5 374,-252.5 438,-252.5 438,-230.5 374,-230.5"/>
<text text-anchor="start" x="399.25" y="-236.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">str</text>
<polygon fill="none" stroke="black" points="312,-208.5 312,-230.5 374,-230.5 374,-208.5 312,-208.5"/>
<text text-anchor="start" x="315.62" y="-214.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">alignment</text>
<polygon fill="none" stroke="black" points="374,-208.5 374,-230.5 438,-230.5 438,-208.5 374,-208.5"/>
<text text-anchor="start" x="376.75" y="-214.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">Alignment</text>
<polygon fill="none" stroke="black" points="312,-186.5 312,-208.5 374,-208.5 374,-186.5 312,-186.5"/>
<text text-anchor="start" x="329.88" y="-192.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">level</text>
<polygon fill="none" stroke="black" points="374,-186.5 374,-208.5 438,-208.5 438,-186.5 374,-186.5"/>
<text text-anchor="start" x="398.88" y="-192.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">int</text>
<polygon fill="none" stroke="black" points="313.12,-279.25 313.12,-301.75 439.88,-301.75 439.88,-279.25 313.12,-279.25"/>
<text text-anchor="start" x="342.75" y="-286.45" font-family="Times New Roman,Times,Liberation Serif,serif" font-weight="bold" font-size="14.00">Adventurer</text>
<polygon fill="none" stroke="black" points="313.12,-256.75 313.12,-279.25 375.38,-279.25 375.38,-256.75 313.12,-256.75"/>
<text text-anchor="start" x="329.25" y="-262.95" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">name</text>
<polygon fill="none" stroke="black" points="375.38,-256.75 375.38,-279.25 439.88,-279.25 439.88,-256.75 375.38,-256.75"/>
<text text-anchor="start" x="400.88" y="-262.95" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">str</text>
<polygon fill="none" stroke="black" points="313.12,-234.25 313.12,-256.75 375.38,-256.75 375.38,-234.25 313.12,-234.25"/>
<text text-anchor="start" x="316.12" y="-240.45" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">profession</text>
<polygon fill="none" stroke="black" points="375.38,-234.25 375.38,-256.75 439.88,-256.75 439.88,-234.25 375.38,-234.25"/>
<text text-anchor="start" x="400.88" y="-240.45" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">str</text>
<polygon fill="none" stroke="black" points="313.12,-211.75 313.12,-234.25 375.38,-234.25 375.38,-211.75 313.12,-211.75"/>
<text text-anchor="start" x="316.88" y="-217.95" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">alignment</text>
<polygon fill="none" stroke="black" points="375.38,-211.75 375.38,-234.25 439.88,-234.25 439.88,-211.75 375.38,-211.75"/>
<text text-anchor="start" x="378.38" y="-217.95" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">Alignment</text>
<polygon fill="none" stroke="black" points="313.12,-189.25 313.12,-211.75 375.38,-211.75 375.38,-189.25 313.12,-189.25"/>
<text text-anchor="start" x="331.12" y="-195.45" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">level</text>
<polygon fill="none" stroke="black" points="375.38,-189.25 375.38,-211.75 439.88,-211.75 439.88,-189.25 375.38,-189.25"/>
<text text-anchor="start" x="400.5" y="-195.45" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">int</text>
</a>

@@ -43,20 +43,20 @@ </g>

<g id="a_node2"><a xlink:title="erdantic.examples.pydantic.Party&#10;&#10;A group of adventurers finding themselves doing and saying things altogether unexpected.&#10;&#10;Attributes:&#10; &#160;&#160;&#160;name (str): Name that party is known by&#10; &#160;&#160;&#160;formed_datetime (datetime): Timestamp of when the party was formed&#10; &#160;&#160;&#160;members (List[Adventurer]): Adventurers that belong to this party&#10; &#160;&#160;&#160;active_quest (Optional[Quest]): Current quest that party is actively tackling&#10;">
<polygon fill="none" stroke="black" points="0,-278.5 0,-300.5 196,-300.5 196,-278.5 0,-278.5"/>
<text text-anchor="start" x="81.88" y="-285.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-weight="bold" font-size="14.00">Party</text>
<polygon fill="none" stroke="black" points="0,-256.5 0,-278.5 99,-278.5 99,-256.5 0,-256.5"/>
<text text-anchor="start" x="34.5" y="-262.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">name</text>
<polygon fill="none" stroke="black" points="99,-256.5 99,-278.5 196,-278.5 196,-256.5 99,-256.5"/>
<text text-anchor="start" x="140.75" y="-262.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">str</text>
<polygon fill="none" stroke="black" points="0,-234.5 0,-256.5 99,-256.5 99,-234.5 0,-234.5"/>
<text text-anchor="start" x="2.62" y="-240.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">formed_datetime</text>
<polygon fill="none" stroke="black" points="99,-234.5 99,-256.5 196,-256.5 196,-234.5 99,-234.5"/>
<text text-anchor="start" x="123.88" y="-240.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">datetime</text>
<polygon fill="none" stroke="black" points="0,-212.5 0,-234.5 99,-234.5 99,-212.5 0,-212.5"/>
<text text-anchor="start" x="24" y="-218.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">members</text>
<polygon fill="none" stroke="black" points="99,-212.5 99,-234.5 196,-234.5 196,-212.5 99,-212.5"/>
<text text-anchor="start" x="101.75" y="-218.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">List[Adventurer]</text>
<polygon fill="none" stroke="black" points="0,-190.5 0,-212.5 99,-212.5 99,-190.5 0,-190.5"/>
<text text-anchor="start" x="15.75" y="-196.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">active_quest</text>
<polygon fill="none" stroke="black" points="99,-190.5 99,-212.5 196,-212.5 196,-190.5 99,-190.5"/>
<text text-anchor="start" x="103.62" y="-196.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">Optional[Quest]</text>
<polygon fill="none" stroke="black" points="0,-283.25 0,-305.75 197.25,-305.75 197.25,-283.25 0,-283.25"/>
<text text-anchor="start" x="82.5" y="-290.45" font-family="Times New Roman,Times,Liberation Serif,serif" font-weight="bold" font-size="14.00">Party</text>
<polygon fill="none" stroke="black" points="0,-260.75 0,-283.25 99.75,-283.25 99.75,-260.75 0,-260.75"/>
<text text-anchor="start" x="34.88" y="-266.95" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">name</text>
<polygon fill="none" stroke="black" points="99.75,-260.75 99.75,-283.25 197.25,-283.25 197.25,-260.75 99.75,-260.75"/>
<text text-anchor="start" x="141.75" y="-266.95" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">str</text>
<polygon fill="none" stroke="black" points="0,-238.25 0,-260.75 99.75,-260.75 99.75,-238.25 0,-238.25"/>
<text text-anchor="start" x="3" y="-244.45" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">formed_datetime</text>
<polygon fill="none" stroke="black" points="99.75,-238.25 99.75,-260.75 197.25,-260.75 197.25,-238.25 99.75,-238.25"/>
<text text-anchor="start" x="124.88" y="-244.45" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">datetime</text>
<polygon fill="none" stroke="black" points="0,-215.75 0,-238.25 99.75,-238.25 99.75,-215.75 0,-215.75"/>
<text text-anchor="start" x="24.38" y="-221.95" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">members</text>
<polygon fill="none" stroke="black" points="99.75,-215.75 99.75,-238.25 197.25,-238.25 197.25,-215.75 99.75,-215.75"/>
<text text-anchor="start" x="102.75" y="-221.95" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">List[Adventurer]</text>
<polygon fill="none" stroke="black" points="0,-193.25 0,-215.75 99.75,-215.75 99.75,-193.25 0,-193.25"/>
<text text-anchor="start" x="16.12" y="-199.45" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">active_quest</text>
<polygon fill="none" stroke="black" points="99.75,-193.25 99.75,-215.75 197.25,-215.75 197.25,-193.25 99.75,-193.25"/>
<text text-anchor="start" x="104.62" y="-199.45" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">Optional[Quest]</text>
</a>

@@ -68,7 +68,7 @@ </g>

<title>erdantic.examples.pydantic.Party:e&#45;&gt;erdantic.examples.pydantic.Adventurer:w</title>
<path fill="none" stroke="black" d="M205.92,-224.15C249.25,-230.11 255.51,-274.88 295.89,-283.91"/>
<polyline fill="none" stroke="black" points="196,-223.5 200.99,-223.83"/>
<polyline fill="none" stroke="black" points="200.99,-223.83 205.98,-224.16"/>
<polygon fill="black" stroke="black" points="300.72,-284.42 310.2,-289.94 306.03,-284.98 310.33,-285.43 310.33,-285.43 310.33,-285.43 306.03,-284.98 311.14,-280.99 300.72,-284.42"/>
<polyline fill="none" stroke="black" points="299.51,-284.29 294.54,-283.77"/>
<path fill="none" stroke="black" d="M207.21,-227.67C250.68,-233.77 256.49,-279.62 296.96,-288.88"/>
<polyline fill="none" stroke="black" points="197.25,-227 202.24,-227.34"/>
<polyline fill="none" stroke="black" points="202.24,-227.34 207.23,-227.67"/>
<polygon fill="black" stroke="black" points="301.85,-289.4 311.31,-294.94 307.15,-289.97 311.46,-290.43 311.46,-290.43 311.46,-290.43 307.15,-289.97 312.27,-285.99 301.85,-289.4"/>
<polyline fill="none" stroke="black" points="300.64,-289.27 295.67,-288.74"/>
</g>

@@ -79,16 +79,16 @@ <!-- erdantic.examples.pydantic.Quest -->

<g id="a_node3"><a xlink:title="erdantic.examples.pydantic.Quest&#10;&#10;A task to complete, with some monetary reward.&#10;&#10;Attributes:&#10; &#160;&#160;&#160;name (str): Name by which this quest is referred to&#10; &#160;&#160;&#160;giver (QuestGiver): Person who offered the quest&#10; &#160;&#160;&#160;reward_gold (int): Amount of gold to be rewarded for quest completion&#10;">
<polygon fill="none" stroke="black" points="304,-128.5 304,-150.5 446,-150.5 446,-128.5 304,-128.5"/>
<text text-anchor="start" x="357.75" y="-135.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-weight="bold" font-size="14.00">Quest</text>
<polygon fill="none" stroke="black" points="304,-106.5 304,-128.5 378,-128.5 378,-106.5 304,-106.5"/>
<text text-anchor="start" x="326" y="-112.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">name</text>
<polygon fill="none" stroke="black" points="378,-106.5 378,-128.5 446,-128.5 446,-106.5 378,-106.5"/>
<text text-anchor="start" x="405.25" y="-112.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">str</text>
<polygon fill="none" stroke="black" points="304,-84.5 304,-106.5 378,-106.5 378,-84.5 304,-84.5"/>
<text text-anchor="start" x="327.12" y="-90.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">giver</text>
<polygon fill="none" stroke="black" points="378,-84.5 378,-106.5 446,-106.5 446,-84.5 378,-84.5"/>
<text text-anchor="start" x="380.88" y="-90.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">QuestGiver</text>
<polygon fill="none" stroke="black" points="304,-62.5 304,-84.5 378,-84.5 378,-62.5 304,-62.5"/>
<text text-anchor="start" x="306.88" y="-68.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">reward_gold</text>
<polygon fill="none" stroke="black" points="378,-62.5 378,-84.5 446,-84.5 446,-62.5 378,-62.5"/>
<text text-anchor="start" x="404.88" y="-68.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">int</text>
<polygon fill="none" stroke="black" points="305.25,-131 305.25,-153.5 447.75,-153.5 447.75,-131 305.25,-131"/>
<text text-anchor="start" x="359.25" y="-138.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-weight="bold" font-size="14.00">Quest</text>
<polygon fill="none" stroke="black" points="305.25,-108.5 305.25,-131 379.5,-131 379.5,-108.5 305.25,-108.5"/>
<text text-anchor="start" x="327.38" y="-114.7" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">name</text>
<polygon fill="none" stroke="black" points="379.5,-108.5 379.5,-131 447.75,-131 447.75,-108.5 379.5,-108.5"/>
<text text-anchor="start" x="406.88" y="-114.7" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">str</text>
<polygon fill="none" stroke="black" points="305.25,-86 305.25,-108.5 379.5,-108.5 379.5,-86 305.25,-86"/>
<text text-anchor="start" x="328.5" y="-92.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">giver</text>
<polygon fill="none" stroke="black" points="379.5,-86 379.5,-108.5 447.75,-108.5 447.75,-86 379.5,-86"/>
<text text-anchor="start" x="382.5" y="-92.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">QuestGiver</text>
<polygon fill="none" stroke="black" points="305.25,-63.5 305.25,-86 379.5,-86 379.5,-63.5 305.25,-63.5"/>
<text text-anchor="start" x="308.25" y="-69.7" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">reward_gold</text>
<polygon fill="none" stroke="black" points="379.5,-63.5 379.5,-86 447.75,-86 447.75,-63.5 379.5,-63.5"/>
<text text-anchor="start" x="406.5" y="-69.7" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">int</text>
</a>

@@ -100,9 +100,9 @@ </g>

<title>erdantic.examples.pydantic.Party:e&#45;&gt;erdantic.examples.pydantic.Quest:w</title>
<path fill="none" stroke="black" d="M205.73,-200.8C245.01,-194.92 251.04,-153.51 285.33,-142.29"/>
<polyline fill="none" stroke="black" points="196,-201.5 200.99,-201.14"/>
<polyline fill="none" stroke="black" points="200.99,-201.14 205.97,-200.79"/>
<polyline fill="none" stroke="black" points="304,-139.5 299.06,-140.24"/>
<polygon fill="black" stroke="black" points="297.33,-135.44 298.81,-145.33 296.83,-145.63 295.35,-135.74 297.33,-135.44"/>
<polyline fill="none" stroke="black" points="299.06,-140.24 294.11,-140.98"/>
<ellipse fill="none" stroke="black" cx="289.66" cy="-141.65" rx="4" ry="4"/>
<path fill="none" stroke="black" d="M206.98,-203.8C246.3,-197.9 252.26,-156.32 286.57,-145.06"/>
<polyline fill="none" stroke="black" points="197.25,-204.5 202.24,-204.14"/>
<polyline fill="none" stroke="black" points="202.24,-204.14 207.22,-203.78"/>
<polyline fill="none" stroke="black" points="305.25,-142.25 300.31,-142.99"/>
<polygon fill="black" stroke="black" points="298.57,-138.2 300.06,-148.09 298.08,-148.38 296.6,-138.49 298.57,-138.2"/>
<polyline fill="none" stroke="black" points="300.31,-142.99 295.36,-143.74"/>
<ellipse fill="none" stroke="black" cx="290.91" cy="-144.4" rx="4" ry="4"/>
</g>

@@ -113,16 +113,16 @@ <!-- erdantic.examples.pydantic.QuestGiver -->

<g id="a_node4"><a xlink:title="erdantic.examples.pydantic.QuestGiver&#10;&#10;A person who offers a task that needs completing.&#10;&#10;Attributes:&#10; &#160;&#160;&#160;name (str): Name of this quest giver&#10; &#160;&#160;&#160;faction (str): Faction that this quest giver belongs to&#10; &#160;&#160;&#160;location (str): Location this quest giver can be found&#10;">
<polygon fill="none" stroke="black" points="554,-84.5 554,-106.5 678,-106.5 678,-84.5 554,-84.5"/>
<text text-anchor="start" x="581.88" y="-91.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-weight="bold" font-size="14.00">QuestGiver</text>
<polygon fill="none" stroke="black" points="554,-62.5 554,-84.5 603,-84.5 603,-62.5 554,-62.5"/>
<text text-anchor="start" x="563.5" y="-68.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">name</text>
<polygon fill="none" stroke="black" points="603,-62.5 603,-84.5 678,-84.5 678,-62.5 603,-62.5"/>
<text text-anchor="start" x="633.75" y="-68.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">str</text>
<polygon fill="none" stroke="black" points="554,-40.5 554,-62.5 603,-62.5 603,-40.5 554,-40.5"/>
<text text-anchor="start" x="559.75" y="-46.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">faction</text>
<polygon fill="none" stroke="black" points="603,-40.5 603,-62.5 678,-62.5 678,-40.5 603,-40.5"/>
<text text-anchor="start" x="605.62" y="-46.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">Optional[str]</text>
<polygon fill="none" stroke="black" points="554,-18.5 554,-40.5 603,-40.5 603,-18.5 554,-18.5"/>
<text text-anchor="start" x="556.75" y="-24.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">location</text>
<polygon fill="none" stroke="black" points="603,-18.5 603,-40.5 678,-40.5 678,-18.5 603,-18.5"/>
<text text-anchor="start" x="633.75" y="-24.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">str</text>
<polygon fill="none" stroke="black" points="555.75,-86 555.75,-108.5 681,-108.5 681,-86 555.75,-86"/>
<text text-anchor="start" x="584.25" y="-93.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-weight="bold" font-size="14.00">QuestGiver</text>
<polygon fill="none" stroke="black" points="555.75,-63.5 555.75,-86 605.25,-86 605.25,-63.5 555.75,-63.5"/>
<text text-anchor="start" x="565.5" y="-69.7" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">name</text>
<polygon fill="none" stroke="black" points="605.25,-63.5 605.25,-86 681,-86 681,-63.5 605.25,-63.5"/>
<text text-anchor="start" x="636.38" y="-69.7" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">str</text>
<polygon fill="none" stroke="black" points="555.75,-41 555.75,-63.5 605.25,-63.5 605.25,-41 555.75,-41"/>
<text text-anchor="start" x="561.75" y="-47.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">faction</text>
<polygon fill="none" stroke="black" points="605.25,-41 605.25,-63.5 681,-63.5 681,-41 605.25,-41"/>
<text text-anchor="start" x="608.25" y="-47.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">Optional[str]</text>
<polygon fill="none" stroke="black" points="555.75,-18.5 555.75,-41 605.25,-41 605.25,-18.5 555.75,-18.5"/>
<text text-anchor="start" x="558.75" y="-24.7" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">location</text>
<polygon fill="none" stroke="black" points="605.25,-18.5 605.25,-41 681,-41 681,-18.5 605.25,-18.5"/>
<text text-anchor="start" x="636.38" y="-24.7" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">str</text>
</a>

@@ -134,12 +134,12 @@ </g>

<title>erdantic.examples.pydantic.Quest:e&#45;&gt;erdantic.examples.pydantic.QuestGiver:w</title>
<path fill="none" stroke="black" d="M455.87,-95.5C491.07,-95.5 505.8,-95.5 539.08,-95.5"/>
<polyline fill="none" stroke="black" points="446,-95.5 451,-95.5"/>
<polyline fill="none" stroke="black" points="451,-95.5 456,-95.5"/>
<polyline fill="none" stroke="black" points="554,-95.5 549,-95.5"/>
<polygon fill="black" stroke="black" points="548,-90.5 548,-100.5 546,-100.5 546,-90.5 548,-90.5"/>
<polyline fill="none" stroke="black" points="549,-95.5 544,-95.5"/>
<polygon fill="black" stroke="black" points="543,-90.5 543,-100.5 541,-100.5 541,-90.5 543,-90.5"/>
<polyline fill="none" stroke="black" points="544,-95.5 539,-95.5"/>
<path fill="none" stroke="black" d="M457.62,-97.25C492.82,-97.25 507.55,-97.25 540.83,-97.25"/>
<polyline fill="none" stroke="black" points="447.75,-97.25 452.75,-97.25"/>
<polyline fill="none" stroke="black" points="452.75,-97.25 457.75,-97.25"/>
<polyline fill="none" stroke="black" points="555.75,-97.25 550.75,-97.25"/>
<polygon fill="black" stroke="black" points="549.75,-92.25 549.75,-102.25 547.75,-102.25 547.75,-92.25 549.75,-92.25"/>
<polyline fill="none" stroke="black" points="550.75,-97.25 545.75,-97.25"/>
<polygon fill="black" stroke="black" points="544.75,-92.25 544.75,-102.25 542.75,-102.25 542.75,-92.25 544.75,-92.25"/>
<polyline fill="none" stroke="black" points="545.75,-97.25 540.75,-97.25"/>
</g>
</g>
</svg>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 9.0.0 (0)
<!-- Generated by graphviz version 12.2.1 (0)
-->
<!-- Title: Entity Relationship Diagram created by erdantic Pages: 1 -->
<svg width="879pt" height="309pt"
viewBox="0.00 0.00 879.00 308.50" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 304.5)">
<svg width="883pt" height="314pt"
viewBox="0.00 0.00 882.50 313.75" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 309.75)">
<title>Entity Relationship Diagram created by erdantic</title>
<polygon fill="white" stroke="none" points="-4,4 -4,-304.5 875,-304.5 875,4 -4,4"/>
<text text-anchor="middle" x="435.5" y="-5.95" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="9.00" fill="#a8a8a8">Created by erdantic vTEST &lt;https://github.com/drivendataorg/erdantic&gt;</text>
<polygon fill="white" stroke="none" points="-4,4 -4,-309.75 878.5,-309.75 878.5,4 -4,4"/>
<text text-anchor="middle" x="437.25" y="-5.95" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="9.00" fill="#a8a8a8">Created by erdantic vTEST &lt;https://github.com/drivendataorg/erdantic&gt;</text>
<!-- erdantic.examples.pydantic.Adventurer -->

@@ -17,25 +17,25 @@ <g id="node1" class="node">

<g id="a_node1"><a xlink:title="erdantic.examples.pydantic.Adventurer&#10;&#10;A person often late for dinner but with a tale or two to tell.&#10;&#10;Attributes:&#10; &#160;&#160;&#160;name (str): Name of this adventurer&#10; &#160;&#160;&#160;profession (str): Profession of this adventurer&#10; &#160;&#160;&#160;alignment (Alignment): Alignment of this adventurer&#10; &#160;&#160;&#160;level (int): Level of this adventurer&#10;">
<polygon fill="none" stroke="black" points="348,-274.5 348,-296.5 510,-296.5 510,-274.5 348,-274.5"/>
<text text-anchor="start" x="395.25" y="-281.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-weight="bold" font-size="14.00">Adventurer</text>
<polygon fill="none" stroke="black" points="348,-252.5 348,-274.5 410,-274.5 410,-252.5 348,-252.5"/>
<text text-anchor="start" x="364" y="-258.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">name</text>
<polygon fill="none" stroke="black" points="410,-252.5 410,-274.5 474,-274.5 474,-252.5 410,-252.5"/>
<text text-anchor="start" x="435.25" y="-258.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">str</text>
<polygon fill="none" stroke="black" points="474,-252.5 474,-274.5 510,-274.5 510,-252.5 474,-252.5"/>
<polygon fill="none" stroke="black" points="348,-230.5 348,-252.5 410,-252.5 410,-230.5 348,-230.5"/>
<text text-anchor="start" x="350.88" y="-236.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">profession</text>
<polygon fill="none" stroke="black" points="410,-230.5 410,-252.5 474,-252.5 474,-230.5 410,-230.5"/>
<text text-anchor="start" x="435.25" y="-236.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">str</text>
<polygon fill="none" stroke="black" points="474,-230.5 474,-252.5 510,-252.5 510,-230.5 474,-230.5"/>
<polygon fill="none" stroke="black" points="348,-208.5 348,-230.5 410,-230.5 410,-208.5 348,-208.5"/>
<text text-anchor="start" x="351.62" y="-214.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">alignment</text>
<polygon fill="none" stroke="black" points="410,-208.5 410,-230.5 474,-230.5 474,-208.5 410,-208.5"/>
<text text-anchor="start" x="412.75" y="-214.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">Alignment</text>
<polygon fill="none" stroke="black" points="474,-208.5 474,-230.5 510,-230.5 510,-208.5 474,-208.5"/>
<polygon fill="none" stroke="black" points="348,-186.5 348,-208.5 410,-208.5 410,-186.5 348,-186.5"/>
<text text-anchor="start" x="365.88" y="-192.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">level</text>
<polygon fill="none" stroke="black" points="410,-186.5 410,-208.5 474,-208.5 474,-186.5 410,-186.5"/>
<text text-anchor="start" x="434.88" y="-192.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">int</text>
<polygon fill="none" stroke="black" points="474,-186.5 474,-208.5 510,-208.5 510,-186.5 474,-186.5"/>
<text text-anchor="start" x="488.62" y="-192.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">1</text>
<polygon fill="none" stroke="black" points="349.12,-279.25 349.12,-301.75 511.88,-301.75 511.88,-279.25 349.12,-279.25"/>
<text text-anchor="start" x="396.75" y="-286.45" font-family="Times New Roman,Times,Liberation Serif,serif" font-weight="bold" font-size="14.00">Adventurer</text>
<polygon fill="none" stroke="black" points="349.12,-256.75 349.12,-279.25 411.38,-279.25 411.38,-256.75 349.12,-256.75"/>
<text text-anchor="start" x="365.25" y="-262.95" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">name</text>
<polygon fill="none" stroke="black" points="411.38,-256.75 411.38,-279.25 475.88,-279.25 475.88,-256.75 411.38,-256.75"/>
<text text-anchor="start" x="436.88" y="-262.95" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">str</text>
<polygon fill="none" stroke="black" points="475.88,-256.75 475.88,-279.25 511.88,-279.25 511.88,-256.75 475.88,-256.75"/>
<polygon fill="none" stroke="black" points="349.12,-234.25 349.12,-256.75 411.38,-256.75 411.38,-234.25 349.12,-234.25"/>
<text text-anchor="start" x="352.12" y="-240.45" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">profession</text>
<polygon fill="none" stroke="black" points="411.38,-234.25 411.38,-256.75 475.88,-256.75 475.88,-234.25 411.38,-234.25"/>
<text text-anchor="start" x="436.88" y="-240.45" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">str</text>
<polygon fill="none" stroke="black" points="475.88,-234.25 475.88,-256.75 511.88,-256.75 511.88,-234.25 475.88,-234.25"/>
<polygon fill="none" stroke="black" points="349.12,-211.75 349.12,-234.25 411.38,-234.25 411.38,-211.75 349.12,-211.75"/>
<text text-anchor="start" x="352.88" y="-217.95" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">alignment</text>
<polygon fill="none" stroke="black" points="411.38,-211.75 411.38,-234.25 475.88,-234.25 475.88,-211.75 411.38,-211.75"/>
<text text-anchor="start" x="414.38" y="-217.95" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">Alignment</text>
<polygon fill="none" stroke="black" points="475.88,-211.75 475.88,-234.25 511.88,-234.25 511.88,-211.75 475.88,-211.75"/>
<polygon fill="none" stroke="black" points="349.12,-189.25 349.12,-211.75 411.38,-211.75 411.38,-189.25 349.12,-189.25"/>
<text text-anchor="start" x="367.12" y="-195.45" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">level</text>
<polygon fill="none" stroke="black" points="411.38,-189.25 411.38,-211.75 475.88,-211.75 475.88,-189.25 411.38,-189.25"/>
<text text-anchor="start" x="436.5" y="-195.45" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">int</text>
<polygon fill="none" stroke="black" points="475.88,-189.25 475.88,-211.75 511.88,-211.75 511.88,-189.25 475.88,-189.25"/>
<text text-anchor="start" x="490.5" y="-195.45" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">1</text>
</a>

@@ -48,26 +48,26 @@ </g>

<g id="a_node2"><a xlink:title="erdantic.examples.pydantic.Party&#10;&#10;A group of adventurers finding themselves doing and saying things altogether unexpected.&#10;&#10;Attributes:&#10; &#160;&#160;&#160;name (str): Name that party is known by&#10; &#160;&#160;&#160;formed_datetime (datetime): Timestamp of when the party was formed&#10; &#160;&#160;&#160;members (List[Adventurer]): Adventurers that belong to this party&#10; &#160;&#160;&#160;active_quest (Optional[Quest]): Current quest that party is actively tackling&#10;">
<polygon fill="none" stroke="black" points="0,-278.5 0,-300.5 232,-300.5 232,-278.5 0,-278.5"/>
<text text-anchor="start" x="99.88" y="-285.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-weight="bold" font-size="14.00">Party</text>
<polygon fill="none" stroke="black" points="0,-256.5 0,-278.5 99,-278.5 99,-256.5 0,-256.5"/>
<text text-anchor="start" x="34.5" y="-262.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">name</text>
<polygon fill="none" stroke="black" points="99,-256.5 99,-278.5 196,-278.5 196,-256.5 99,-256.5"/>
<text text-anchor="start" x="140.75" y="-262.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">str</text>
<polygon fill="none" stroke="black" points="196,-256.5 196,-278.5 232,-278.5 232,-256.5 196,-256.5"/>
<polygon fill="none" stroke="black" points="0,-234.5 0,-256.5 99,-256.5 99,-234.5 0,-234.5"/>
<text text-anchor="start" x="2.62" y="-240.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">formed_datetime</text>
<polygon fill="none" stroke="black" points="99,-234.5 99,-256.5 196,-256.5 196,-234.5 99,-234.5"/>
<text text-anchor="start" x="123.88" y="-240.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">datetime</text>
<polygon fill="none" stroke="black" points="196,-234.5 196,-256.5 232,-256.5 232,-234.5 196,-234.5"/>
<polygon fill="none" stroke="black" points="0,-212.5 0,-234.5 99,-234.5 99,-212.5 0,-212.5"/>
<text text-anchor="start" x="24" y="-218.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">members</text>
<polygon fill="none" stroke="black" points="99,-212.5 99,-234.5 196,-234.5 196,-212.5 99,-212.5"/>
<text text-anchor="start" x="101.75" y="-218.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">List[Adventurer]</text>
<polygon fill="none" stroke="black" points="196,-212.5 196,-234.5 232,-234.5 232,-212.5 196,-212.5"/>
<text text-anchor="start" x="209.5" y="-218.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">[]</text>
<polygon fill="none" stroke="black" points="0,-190.5 0,-212.5 99,-212.5 99,-190.5 0,-190.5"/>
<text text-anchor="start" x="15.75" y="-196.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">active_quest</text>
<polygon fill="none" stroke="black" points="99,-190.5 99,-212.5 196,-212.5 196,-190.5 99,-190.5"/>
<text text-anchor="start" x="103.62" y="-196.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">Optional[Quest]</text>
<polygon fill="none" stroke="black" points="196,-190.5 196,-212.5 232,-212.5 232,-190.5 196,-190.5"/>
<text text-anchor="start" x="199.38" y="-196.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">None</text>
<polygon fill="none" stroke="black" points="0,-283.25 0,-305.75 233.25,-305.75 233.25,-283.25 0,-283.25"/>
<text text-anchor="start" x="100.5" y="-290.45" font-family="Times New Roman,Times,Liberation Serif,serif" font-weight="bold" font-size="14.00">Party</text>
<polygon fill="none" stroke="black" points="0,-260.75 0,-283.25 99.75,-283.25 99.75,-260.75 0,-260.75"/>
<text text-anchor="start" x="34.88" y="-266.95" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">name</text>
<polygon fill="none" stroke="black" points="99.75,-260.75 99.75,-283.25 197.25,-283.25 197.25,-260.75 99.75,-260.75"/>
<text text-anchor="start" x="141.75" y="-266.95" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">str</text>
<polygon fill="none" stroke="black" points="197.25,-260.75 197.25,-283.25 233.25,-283.25 233.25,-260.75 197.25,-260.75"/>
<polygon fill="none" stroke="black" points="0,-238.25 0,-260.75 99.75,-260.75 99.75,-238.25 0,-238.25"/>
<text text-anchor="start" x="3" y="-244.45" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">formed_datetime</text>
<polygon fill="none" stroke="black" points="99.75,-238.25 99.75,-260.75 197.25,-260.75 197.25,-238.25 99.75,-238.25"/>
<text text-anchor="start" x="124.88" y="-244.45" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">datetime</text>
<polygon fill="none" stroke="black" points="197.25,-238.25 197.25,-260.75 233.25,-260.75 233.25,-238.25 197.25,-238.25"/>
<polygon fill="none" stroke="black" points="0,-215.75 0,-238.25 99.75,-238.25 99.75,-215.75 0,-215.75"/>
<text text-anchor="start" x="24.38" y="-221.95" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">members</text>
<polygon fill="none" stroke="black" points="99.75,-215.75 99.75,-238.25 197.25,-238.25 197.25,-215.75 99.75,-215.75"/>
<text text-anchor="start" x="102.75" y="-221.95" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">List[Adventurer]</text>
<polygon fill="none" stroke="black" points="197.25,-215.75 197.25,-238.25 233.25,-238.25 233.25,-215.75 197.25,-215.75"/>
<text text-anchor="start" x="210.75" y="-221.95" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">[]</text>
<polygon fill="none" stroke="black" points="0,-193.25 0,-215.75 99.75,-215.75 99.75,-193.25 0,-193.25"/>
<text text-anchor="start" x="16.12" y="-199.45" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">active_quest</text>
<polygon fill="none" stroke="black" points="99.75,-193.25 99.75,-215.75 197.25,-215.75 197.25,-193.25 99.75,-193.25"/>
<text text-anchor="start" x="104.62" y="-199.45" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">Optional[Quest]</text>
<polygon fill="none" stroke="black" points="197.25,-193.25 197.25,-215.75 233.25,-215.75 233.25,-193.25 197.25,-193.25"/>
<text text-anchor="start" x="200.62" y="-199.45" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">None</text>
</a>

@@ -79,7 +79,7 @@ </g>

<title>erdantic.examples.pydantic.Party:e&#45;&gt;erdantic.examples.pydantic.Adventurer:w</title>
<path fill="none" stroke="black" d="M241.92,-224.15C285.25,-230.11 291.51,-274.88 331.89,-283.91"/>
<polyline fill="none" stroke="black" points="232,-223.5 236.99,-223.83"/>
<polyline fill="none" stroke="black" points="236.99,-223.83 241.98,-224.16"/>
<polygon fill="black" stroke="black" points="336.72,-284.42 346.2,-289.94 342.03,-284.98 346.33,-285.43 346.33,-285.43 346.33,-285.43 342.03,-284.98 347.14,-280.99 336.72,-284.42"/>
<polyline fill="none" stroke="black" points="335.51,-284.29 330.54,-283.77"/>
<path fill="none" stroke="black" d="M243.21,-227.67C286.68,-233.77 292.49,-279.62 332.96,-288.88"/>
<polyline fill="none" stroke="black" points="233.25,-227 238.24,-227.34"/>
<polyline fill="none" stroke="black" points="238.24,-227.34 243.23,-227.67"/>
<polygon fill="black" stroke="black" points="337.85,-289.4 347.31,-294.94 343.15,-289.97 347.46,-290.43 347.46,-290.43 347.46,-290.43 343.15,-289.97 348.27,-285.99 337.85,-289.4"/>
<polyline fill="none" stroke="black" points="336.64,-289.27 331.67,-288.74"/>
</g>

@@ -90,20 +90,20 @@ <!-- erdantic.examples.pydantic.Quest -->

<g id="a_node3"><a xlink:title="erdantic.examples.pydantic.Quest&#10;&#10;A task to complete, with some monetary reward.&#10;&#10;Attributes:&#10; &#160;&#160;&#160;name (str): Name by which this quest is referred to&#10; &#160;&#160;&#160;giver (QuestGiver): Person who offered the quest&#10; &#160;&#160;&#160;reward_gold (int): Amount of gold to be rewarded for quest completion&#10;">
<polygon fill="none" stroke="black" points="340,-128.5 340,-150.5 518,-150.5 518,-128.5 340,-128.5"/>
<text text-anchor="start" x="411.75" y="-135.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-weight="bold" font-size="14.00">Quest</text>
<polygon fill="none" stroke="black" points="340,-106.5 340,-128.5 414,-128.5 414,-106.5 340,-106.5"/>
<text text-anchor="start" x="362" y="-112.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">name</text>
<polygon fill="none" stroke="black" points="414,-106.5 414,-128.5 482,-128.5 482,-106.5 414,-106.5"/>
<text text-anchor="start" x="441.25" y="-112.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">str</text>
<polygon fill="none" stroke="black" points="482,-106.5 482,-128.5 518,-128.5 518,-106.5 482,-106.5"/>
<polygon fill="none" stroke="black" points="340,-84.5 340,-106.5 414,-106.5 414,-84.5 340,-84.5"/>
<text text-anchor="start" x="363.12" y="-90.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">giver</text>
<polygon fill="none" stroke="black" points="414,-84.5 414,-106.5 482,-106.5 482,-84.5 414,-84.5"/>
<text text-anchor="start" x="416.88" y="-90.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">QuestGiver</text>
<polygon fill="none" stroke="black" points="482,-84.5 482,-106.5 518,-106.5 518,-84.5 482,-84.5"/>
<polygon fill="none" stroke="black" points="340,-62.5 340,-84.5 414,-84.5 414,-62.5 340,-62.5"/>
<text text-anchor="start" x="342.88" y="-68.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">reward_gold</text>
<polygon fill="none" stroke="black" points="414,-62.5 414,-84.5 482,-84.5 482,-62.5 414,-62.5"/>
<text text-anchor="start" x="440.88" y="-68.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">int</text>
<polygon fill="none" stroke="black" points="482,-62.5 482,-84.5 518,-84.5 518,-62.5 482,-62.5"/>
<text text-anchor="start" x="489.88" y="-68.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">100</text>
<polygon fill="none" stroke="black" points="341.25,-131 341.25,-153.5 519.75,-153.5 519.75,-131 341.25,-131"/>
<text text-anchor="start" x="413.25" y="-138.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-weight="bold" font-size="14.00">Quest</text>
<polygon fill="none" stroke="black" points="341.25,-108.5 341.25,-131 415.5,-131 415.5,-108.5 341.25,-108.5"/>
<text text-anchor="start" x="363.38" y="-114.7" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">name</text>
<polygon fill="none" stroke="black" points="415.5,-108.5 415.5,-131 483.75,-131 483.75,-108.5 415.5,-108.5"/>
<text text-anchor="start" x="442.88" y="-114.7" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">str</text>
<polygon fill="none" stroke="black" points="483.75,-108.5 483.75,-131 519.75,-131 519.75,-108.5 483.75,-108.5"/>
<polygon fill="none" stroke="black" points="341.25,-86 341.25,-108.5 415.5,-108.5 415.5,-86 341.25,-86"/>
<text text-anchor="start" x="364.5" y="-92.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">giver</text>
<polygon fill="none" stroke="black" points="415.5,-86 415.5,-108.5 483.75,-108.5 483.75,-86 415.5,-86"/>
<text text-anchor="start" x="418.5" y="-92.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">QuestGiver</text>
<polygon fill="none" stroke="black" points="483.75,-86 483.75,-108.5 519.75,-108.5 519.75,-86 483.75,-86"/>
<polygon fill="none" stroke="black" points="341.25,-63.5 341.25,-86 415.5,-86 415.5,-63.5 341.25,-63.5"/>
<text text-anchor="start" x="344.25" y="-69.7" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">reward_gold</text>
<polygon fill="none" stroke="black" points="415.5,-63.5 415.5,-86 483.75,-86 483.75,-63.5 415.5,-63.5"/>
<text text-anchor="start" x="442.5" y="-69.7" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">int</text>
<polygon fill="none" stroke="black" points="483.75,-63.5 483.75,-86 519.75,-86 519.75,-63.5 483.75,-63.5"/>
<text text-anchor="start" x="491.62" y="-69.7" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">100</text>
</a>

@@ -115,9 +115,9 @@ </g>

<title>erdantic.examples.pydantic.Party:e&#45;&gt;erdantic.examples.pydantic.Quest:w</title>
<path fill="none" stroke="black" d="M241.73,-200.8C281.01,-194.92 287.04,-153.51 321.33,-142.29"/>
<polyline fill="none" stroke="black" points="232,-201.5 236.99,-201.14"/>
<polyline fill="none" stroke="black" points="236.99,-201.14 241.97,-200.79"/>
<polyline fill="none" stroke="black" points="340,-139.5 335.06,-140.24"/>
<polygon fill="black" stroke="black" points="333.33,-135.44 334.81,-145.33 332.83,-145.63 331.35,-135.74 333.33,-135.44"/>
<polyline fill="none" stroke="black" points="335.06,-140.24 330.11,-140.98"/>
<ellipse fill="none" stroke="black" cx="325.66" cy="-141.65" rx="4" ry="4"/>
<path fill="none" stroke="black" d="M242.98,-203.8C282.3,-197.9 288.26,-156.32 322.57,-145.06"/>
<polyline fill="none" stroke="black" points="233.25,-204.5 238.24,-204.14"/>
<polyline fill="none" stroke="black" points="238.24,-204.14 243.22,-203.78"/>
<polyline fill="none" stroke="black" points="341.25,-142.25 336.31,-142.99"/>
<polygon fill="black" stroke="black" points="334.57,-138.2 336.06,-148.09 334.08,-148.38 332.6,-138.49 334.57,-138.2"/>
<polyline fill="none" stroke="black" points="336.31,-142.99 331.36,-143.74"/>
<ellipse fill="none" stroke="black" cx="326.91" cy="-144.4" rx="4" ry="4"/>
</g>

@@ -128,21 +128,21 @@ <!-- erdantic.examples.pydantic.QuestGiver -->

<g id="a_node4"><a xlink:title="erdantic.examples.pydantic.QuestGiver&#10;&#10;A person who offers a task that needs completing.&#10;&#10;Attributes:&#10; &#160;&#160;&#160;name (str): Name of this quest giver&#10; &#160;&#160;&#160;faction (str): Faction that this quest giver belongs to&#10; &#160;&#160;&#160;location (str): Location this quest giver can be found&#10;">
<polygon fill="none" stroke="black" points="626,-84.5 626,-106.5 871,-106.5 871,-84.5 626,-84.5"/>
<text text-anchor="start" x="714.38" y="-91.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-weight="bold" font-size="14.00">QuestGiver</text>
<polygon fill="none" stroke="black" points="626,-62.5 626,-84.5 675,-84.5 675,-62.5 626,-62.5"/>
<text text-anchor="start" x="635.5" y="-68.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">name</text>
<polygon fill="none" stroke="black" points="675,-62.5 675,-84.5 750,-84.5 750,-62.5 675,-62.5"/>
<text text-anchor="start" x="705.75" y="-68.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">str</text>
<polygon fill="none" stroke="black" points="750,-62.5 750,-84.5 871,-84.5 871,-62.5 750,-62.5"/>
<polygon fill="none" stroke="black" points="626,-40.5 626,-62.5 675,-62.5 675,-40.5 626,-40.5"/>
<text text-anchor="start" x="631.75" y="-46.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">faction</text>
<polygon fill="none" stroke="black" points="675,-40.5 675,-62.5 750,-62.5 750,-40.5 675,-40.5"/>
<text text-anchor="start" x="677.62" y="-46.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">Optional[str]</text>
<polygon fill="none" stroke="black" points="750,-40.5 750,-62.5 871,-62.5 871,-40.5 750,-40.5"/>
<text text-anchor="start" x="795.88" y="-46.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">None</text>
<polygon fill="none" stroke="black" points="626,-18.5 626,-40.5 675,-40.5 675,-18.5 626,-18.5"/>
<text text-anchor="start" x="628.75" y="-24.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">location</text>
<polygon fill="none" stroke="black" points="675,-18.5 675,-40.5 750,-40.5 750,-18.5 675,-18.5"/>
<text text-anchor="start" x="705.75" y="-24.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">str</text>
<polygon fill="none" stroke="black" points="750,-18.5 750,-40.5 871,-40.5 871,-18.5 750,-18.5"/>
<text text-anchor="start" x="752.75" y="-24.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">&quot;Adventurer&#39;s Guild&quot;</text>
<polygon fill="none" stroke="black" points="627.75,-86 627.75,-108.5 874.5,-108.5 874.5,-86 627.75,-86"/>
<text text-anchor="start" x="717" y="-93.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-weight="bold" font-size="14.00">QuestGiver</text>
<polygon fill="none" stroke="black" points="627.75,-63.5 627.75,-86 677.25,-86 677.25,-63.5 627.75,-63.5"/>
<text text-anchor="start" x="637.5" y="-69.7" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">name</text>
<polygon fill="none" stroke="black" points="677.25,-63.5 677.25,-86 753,-86 753,-63.5 677.25,-63.5"/>
<text text-anchor="start" x="708.38" y="-69.7" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">str</text>
<polygon fill="none" stroke="black" points="753,-63.5 753,-86 874.5,-86 874.5,-63.5 753,-63.5"/>
<polygon fill="none" stroke="black" points="627.75,-41 627.75,-63.5 677.25,-63.5 677.25,-41 627.75,-41"/>
<text text-anchor="start" x="633.75" y="-47.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">faction</text>
<polygon fill="none" stroke="black" points="677.25,-41 677.25,-63.5 753,-63.5 753,-41 677.25,-41"/>
<text text-anchor="start" x="680.25" y="-47.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">Optional[str]</text>
<polygon fill="none" stroke="black" points="753,-41 753,-63.5 874.5,-63.5 874.5,-41 753,-41"/>
<text text-anchor="start" x="799.12" y="-47.2" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">None</text>
<polygon fill="none" stroke="black" points="627.75,-18.5 627.75,-41 677.25,-41 677.25,-18.5 627.75,-18.5"/>
<text text-anchor="start" x="630.75" y="-24.7" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">location</text>
<polygon fill="none" stroke="black" points="677.25,-18.5 677.25,-41 753,-41 753,-18.5 677.25,-18.5"/>
<text text-anchor="start" x="708.38" y="-24.7" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">str</text>
<polygon fill="none" stroke="black" points="753,-18.5 753,-41 874.5,-41 874.5,-18.5 753,-18.5"/>
<text text-anchor="start" x="756" y="-24.7" font-family="Times New Roman,Times,Liberation Serif,serif" font-size="14.00">&quot;Adventurer&#39;s Guild&quot;</text>
</a>

@@ -154,12 +154,12 @@ </g>

<title>erdantic.examples.pydantic.Quest:e&#45;&gt;erdantic.examples.pydantic.QuestGiver:w</title>
<path fill="none" stroke="black" d="M527.87,-95.5C563.07,-95.5 577.8,-95.5 611.08,-95.5"/>
<polyline fill="none" stroke="black" points="518,-95.5 523,-95.5"/>
<polyline fill="none" stroke="black" points="523,-95.5 528,-95.5"/>
<polyline fill="none" stroke="black" points="626,-95.5 621,-95.5"/>
<polygon fill="black" stroke="black" points="620,-90.5 620,-100.5 618,-100.5 618,-90.5 620,-90.5"/>
<polyline fill="none" stroke="black" points="621,-95.5 616,-95.5"/>
<polygon fill="black" stroke="black" points="615,-90.5 615,-100.5 613,-100.5 613,-90.5 615,-90.5"/>
<polyline fill="none" stroke="black" points="616,-95.5 611,-95.5"/>
<path fill="none" stroke="black" d="M529.62,-97.25C564.82,-97.25 579.55,-97.25 612.83,-97.25"/>
<polyline fill="none" stroke="black" points="519.75,-97.25 524.75,-97.25"/>
<polyline fill="none" stroke="black" points="524.75,-97.25 529.75,-97.25"/>
<polyline fill="none" stroke="black" points="627.75,-97.25 622.75,-97.25"/>
<polygon fill="black" stroke="black" points="621.75,-92.25 621.75,-102.25 619.75,-102.25 619.75,-92.25 621.75,-92.25"/>
<polyline fill="none" stroke="black" points="622.75,-97.25 617.75,-97.25"/>
<polygon fill="black" stroke="black" points="616.75,-92.25 616.75,-102.25 614.75,-102.25 614.75,-92.25 616.75,-92.25"/>
<polyline fill="none" stroke="black" points="617.75,-97.25 612.75,-97.25"/>
</g>
</g>
</svg>

@@ -8,2 +8,3 @@ from pathlib import Path

import erdantic.core
import erdantic.plugins

@@ -38,1 +39,31 @@ OUTPUTS_DIR = Path(__file__).resolve().parent / "_outputs"

monkeypatch.setattr(erdantic.core, "__version__", "TEST")
@pytest.fixture()
def custom_plugin():
"""A custom plugin to test the plugin system."""
class CustomBaseModel: ...
def is_custom_model(obj):
return (
isinstance(obj, type)
and issubclass(obj, CustomBaseModel)
and obj is not CustomBaseModel
)
def get_fields_from_custom_model(model):
return []
key = "test_plugin"
erdantic.plugins.register_plugin(
key,
predicate_fn=is_custom_model,
get_fields_fn=get_fields_from_custom_model,
)
yield key, CustomBaseModel, is_custom_model, get_fields_from_custom_model
# Cleanup
if key in erdantic.plugins._dict:
del erdantic.plugins._dict[key]
import filecmp
import re
import subprocess

@@ -29,2 +30,5 @@

import_object_from_name("erdantic.examples.pydantic.not_a_model_class")
with pytest.raises(RuntimeError) as excinfo:
import_object_from_name("tests.module_with_error.SomeModel")
assert str(excinfo.value) == "This is a test exception"

@@ -210,2 +214,17 @@

def test_list_plugins():
result = runner.invoke(app, ["--list-plugins"])
print(result.output)
assert result.exit_code == 0
assert re.findall(r"\[X\]\s*pydantic", result.output)
def test_list_plugins_custom_plugin(custom_plugin):
key, _, _, _ = custom_plugin
result = runner.invoke(app, ["--list-plugins"])
print(result.output)
assert result.exit_code == 0
assert re.findall(rf"\[X\]\s*{key}", result.output)
def test_help():

@@ -212,0 +231,0 @@ """Test the CLI with --help flag."""

@@ -7,9 +7,4 @@ import builtins

import sys
from typing import Any, AnyStr, List, Literal, Optional, Tuple, TypeVar
from typing import Annotated, Any, AnyStr, List, Literal, Optional, Tuple, TypeVar
if sys.version_info >= (3, 9):
from typing import Annotated
else:
from typing_extensions import Annotated
import IPython.lib.pretty as IPython_pretty

@@ -436,3 +431,3 @@ import pydantic

"""Subclass things to add a third column with the field default value."""
out_dir = outputs_dir / "test_score-test_subclass"
out_dir = outputs_dir / "test_core-test_subclass"
out_dir.mkdir()

@@ -439,0 +434,0 @@ filename = "pydantic_with_default_column"

import dataclasses
from dataclasses import dataclass
from pprint import pprint
import sys
from typing import Optional
from typing import Annotated, Optional, get_type_hints
if sys.version_info >= (3, 9):
from typing import Annotated, get_type_hints
else:
from typing_extensions import Annotated, get_type_hints
import pytest

@@ -13,0 +7,0 @@

@@ -13,3 +13,2 @@ import subprocess

list_plugins,
register_plugin,
)

@@ -21,26 +20,14 @@ import erdantic.plugins.attrs

def test_register_plugin():
"""Custom plugin can be sucessfully registered and used."""
def test_register_plugin(custom_plugin):
"""Custom plugin can be successfully registered and used."""
class CustomBaseModel: ...
key, base_model, predicate_fn, get_fields_fn = custom_plugin
def is_custom_model(obj):
return (
isinstance(obj, type)
and issubclass(obj, CustomBaseModel)
and obj is not CustomBaseModel
)
assert key in list_plugins()
def get_fields_from_custom_model(model):
return []
class MyModel(base_model): ...
register_plugin("test_plugin", is_custom_model, get_fields_from_custom_model)
assert "test_plugin" in list_plugins()
class MyModel(CustomBaseModel): ...
assert identify_field_extractor_fn(MyModel) == get_fields_from_custom_model
assert identify_field_extractor_fn(MyModel) == get_fields_fn
assert identify_field_extractor_fn(erdantic.examples.pydantic.Party) not in (
get_fields_from_custom_model,
get_fields_fn,
None,

@@ -99,3 +86,3 @@ )

"""Core plugins are loaded when erdantic is imported."""
for key in ("attrs", "dataclasses", "pydantic", "pydantic_v1"):
for key in ("attrs", "dataclasses", "msgspec", "pydantic", "pydantic_v1"):
script = textwrap.dedent(

@@ -102,0 +89,0 @@ f"""\

name: docs-main
on:
push:
branches: [main]
workflow_dispatch:
jobs:
build:
name: Build docs from main
runs-on: "ubuntu-latest"
steps:
- uses: actions/checkout@v4
- uses: conda-incubator/setup-miniconda@v3
with:
miniforge-version: latest
activate-environment: ""
use-mamba: true
- name: Install nox and uv
run: |
pipx install nox
pipx install uv
- name: Build documentation
run: |
nox -s docs --verbose
- name: Stage docs on gh-pages
working-directory: docs
run: |
git fetch origin gh-pages --depth=1
git config user.name github-actions[bot]
git config user.email 41898282+github-actions[bot]@users.noreply.github.com
RUN_MIKE="conda run -p ../.nox/docs mike"
$RUN_MIKE deploy --push ~latest --title=latest
deploy:
name: Deploy docs to Netlify
needs: build
runs-on: "ubuntu-latest"
steps:
- uses: actions/checkout@v4
with:
ref: gh-pages
- name: Deploy docs to Netlify
uses: nwtgck/actions-netlify@v1.1
with:
publish-dir: "./"
production-deploy: true
github-token: ${{ secrets.GITHUB_TOKEN }}
deploy-message: "Deploy from GitHub Actions"
enable-pull-request-comment: false
enable-commit-comment: false
overwrites-pull-request-comment: false
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
timeout-minutes: 1
# erdantic Changelog
## v1.0.5 (2024-09-19)
- Fixed runtime `AttributeError` that occurred when creating a diagram that includes a model with a field that uses a type annotation with the ellipsis literal (e.g., `tuple[int, ...]`). ([Issue #124](https://github.com/drivendataorg/erdantic/issues/124), [PR #127](https://github.com/drivendataorg/erdantic/pull/127))
## v1.0.4 (2024-07-16)
- Fixed handling of `typing.Annotated` in cases where it's not the outermost generic type. ([Issue #122](https://github.com/drivendataorg/erdantic/issues/122), [PR #123](https://github.com/drivendataorg/erdantic/pull/123))
## v1.0.3 (2024-05-10)
- Fixed `StopIteration` error when rendering a model that has no fields. ([Issue #120](https://github.com/drivendataorg/erdantic/issues/120), [PR #121](https://github.com/drivendataorg/erdantic/pull/121))
## v1.0.2 (2024-04-11)
- Fixed `AttributeError` when adding a model that has a field annotated with certain typing special forms like `Any`, `Literal`, or `TypeVar` instances. ([Issue #114](https://github.com/drivendataorg/erdantic/issues/114), [PR #115](https://github.com/drivendataorg/erdantic/pull/115))
## v1.0.1 (2024-04-10)
- Fixed `ModuleNotFoundError` when importing from `erdantic.examples` without attrs installed.
## v1.0.0.post2 (2024-04-10)
- Fixed missing LICENSE file in sdist.
## v1.0.0.post1 (2024-04-09)
- Fixed outdated note in README.
## v1.0.0 (2024-04-09)
> [!IMPORTANT]
> This release features significant changes to erdantic, primarily to the backend process of analyzing models and representing data. If you have been primarily using the CLI or the convenience functions `create`, `draw`, and `to_dot`, then your code may continue to work without any changes. If you are doing something more advanced, you may need to update your code.
### CLI changes
- Deprecated `--termini` option. Use the new `--terminal-model` option instead. The shorthand option `-t` remains the same. The `--termini` option still works but will emit a deprecation warning.
### Convenience function changes
- Deprecated `termini` argument for `create`, `draw`, and `to_dot` functions. Use the new `terminal_models` argument instead. The `termini` argument still works but will emit a deprecation warning.
- Added `graph_attr`, `node_attr`, and `edge_attr` arguments to the `draw` and `to_dot` functions that allow you to override attributes on the generated pygraphviz object for the diagram.
### Visual changes
A few changes have been made to the visual content of rendered diagrams.
- Changed the extraction of type names to use the [typenames](https://github.com/jayqi/typenames) library. This should generally produce identical rendered outputs as before, with the following exception:
- Removed the special case behavior for rendering enum classes. Enums now just show the class name without inheritance information.
- Changed collection fields (e.g., `List[TargetModel]`) to display as a "many" relationship (crow) instead of a "zero-or-many" relationship (odot + crow), treating the modality of the field as unspecified. A field will only be displayed as "zero-or-many" (odot + crow) if it is explicitly optional, like `Optional[List[TargetModel]]`.
- Fixed incorrect representation of manyness for type annotations where the outermost annotation wasn't a collection type. ([Issue #105](https://github.com/drivendataorg/erdantic/issues/105))
### Support for attrs
- Added support for [attrs](https://www.attrs.org/en/stable/index.html) classes, i.e., classes decorated by `attrs.define`. The source code for attrs support can be found in the new module `erdantic.plugins.attrs`.
- Added new example module `erdantic.examples.attrs`.
### Backend changes
Significant changes have been made to the library backend to more strongly separate the model analysis process, the extracted data, and the diagram rendering process. We believe this more structured design facilitates customizing diagrams and simplifies the implementation for each data modeling framework. Please see the new documentation pages ["Customizing diagrams"](http://erdantic.drivendata.org/v1.0/customizing/) and ["Extending or modifying erdantic"](http://erdantic.drivendata.org/v1.0/extending/) for details on the new design.
A summary of some key changes is below:
- Removed the adapter base classes `Model` and `Field` and the conrete adapters `DataClassModel`, `DataClassField`, `PydanticModel`, and `PydanticField`.
- Added new Pydantic models `ModelInfo` and `FieldInfo` to replace the adapter system. These new models hold static data that have been extracted from models that erdantic analyzed.
- Removed the adapter system and associated objects such as `model_adapter_registry` and `register_model_adapter`.
- Added new plugin system to replace the adapter system as the way that modeling frameworks are supported. Plugins must implement two functions—a predicate function and a field extractor function—and be registered using `register_plugin`. All objects related to plugins can be found in the new `erdantic.plugins` module and its submodules.
- Renamed `erdantic.typing` module to `erdantic.typing_utils`.
### Other
- Added [PEP 561 `py.typed` marker file](https://peps.python.org/pep-0561/#packaging-type-information) to indicate that the package supports type checking.
- Added IPython special method for pretty-print string representations of `EntityRelationshipDiagram` instances.
- Removed support for Python 3.7. ([PR #102](https://github.com/drivendataorg/erdantic/pull/102))
## v0.7.1 (2024-04-09)
This will be the last version that supports Python 3.7.
- Added version typer version ceiling of `< 0.10.0` due to incompatibility with a fix introduced in that version.
## v0.7.0 (2024-02-11)
- Added support for Pydantic V1 legacy models. These are models created from the `pydantic.v1` namespace when Pydantic V2 is installed. ([PR #94](https://github.com/drivendataorg/erdantic/pull/94) from [@ursereg](https://github.com/ursereg))
## v0.6.0 (2023-07-09)
- Added support for Pydantic V2.
- Removed support for Pydantic V1.
- Changed the init signature for `PydanticField` to work with Pydantic V2's API.
- Added `is_many` and `is_nullable` functions to `erdantic.typing`.
## v0.5.1 (2023-07-04)
- Changed Pydantic dependency to be `< 2`. This will be the final version of erdantic that supports Pydantic V1.
- Changed to pyproject.toml-based build.
## v0.5.0 (2022-07-29)
- Removed support for Python 3.6. ([Issue #51](https://github.com/drivendataorg/erdantic/issues/51), [PR #56](https://github.com/drivendataorg/erdantic/pull/56))
- Added support for modules as inputs to all entrypoints to diagram creation (`create`, `draw`, `to_dot`, CLI). For all modules passed, erdantic will find all supported data model classes in each module. ([Issue #23](https://github.com/drivendataorg/erdantic/issues/23), [PR #58](https://github.com/drivendataorg/erdantic/pull/58))
- Added new parameter `limit_search_models_to` to all entrypoints to allow for limiting which data model classes will be yielded from searching a module.
## v0.4.1 (2022-04-08)
- Fixed error when rendering a data model that has field using `typing.Literal`. ([PR #49](https://github.com/drivendataorg/erdantic/pull/49))
## v0.4.0 (2021-11-06)
- Added support for showing field documentation from Pydantic models with descriptions set with `Field(description=...)` in SVG tooltips. This will add an "Attributes" section to the tooltip using Google-style docstring format and lists fields where the `description` keyword argument is used. ([Issue #8](https://github.com/drivendataorg/erdantic/issues/8#issuecomment-958905131), [PR #42](https://github.com/drivendataorg/erdantic/pull/42))
## v0.3.0 (2021-10-28)
- Fixed handling of forward references in field type declarations. Evaluated forward references will be properly identified. Forward references not converted to `typing.ForwardRef` will throw a `StringForwardRefError` with instructions for how to resolve. Unevaluated forward references will throw an `UnevaluatedForwardRefError` with instructions for how to resolve. See [new documentation](https://erdantic.drivendata.org/stable/forward-references/) for more details. ([Issue #40](https://github.com/drivendataorg/erdantic/issues/40), [PR #41](https://github.com/drivendataorg/erdantic/pull/41))
- Changed name of `erdantic.errors` module to `erdantic.exceptions`. ([PR #41](https://github.com/drivendataorg/erdantic/issues/41))
- Added new `ErdanticException` base class from which other exceptions raised within the erdantic library are subclassed from. Changed several existing `ValueError` exceptions to new exception classes that subclass both `ErdanticException` and `ValueError`. ([PR #41](https://github.com/drivendataorg/erdantic/issues/41))
- Changed `__lt__` method on `Model` and `Edge` to return `NotImplemented` instead of raising an exception to follow typical convention for unsupported input types. ([PR #41](https://github.com/drivendataorg/erdantic/issues/41))
## v0.2.1 (2021-02-16)
- Fixed runtime error when rendering a data model that had a field containing `typing.Any`. ([Issue #25](https://github.com/drivendataorg/erdantic/issues/25), [PR #26](https://github.com/drivendataorg/erdantic/issues/26))
## v0.2.0 (2021-02-14)
- Added option to specify models as terminal nodes. This allows you to truncate large diagrams and split them up into smaller ones. ([PR #24](https://github.com/drivendataorg/erdantic/pull/24))
## v0.1.2 (2021-02-10)
- Fixed bug where Pydantic fields were missing generics in their type annotations. ([PR #19](https://github.com/drivendataorg/erdantic/pull/19))
- Added tests against static rendered DOT output. Change adapter tests to use parameterized fixtures. ([PR #21](https://github.com/drivendataorg/erdantic/pull/21))
## v0.1.1 (2021-02-10)
- Fixed rendered example image in the package description on PyPI. ([PR #18](https://github.com/drivendataorg/erdantic/pull/18))
## v0.1.0 (2021-02-10)
Initial release! 🎉
.PHONY: clean clean-docs clean-pyc clean-test clean-build docs format lint release release-test test help
.DEFAULT_GOAL := help
define PRINT_HELP_PYSCRIPT
import re, sys
for line in sys.stdin:
match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line)
if match:
target, help = match.groups()
print("%-20s %s" % (target, help))
endef
export PRINT_HELP_PYSCRIPT
clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts
clean-build: ## remove build artifacts
rm -fr build/
rm -fr dist/
rm -fr .eggs/
find . -name '*.egg-info' -exec rm -fr {} +
find . -name '*.egg' -exec rm -f {} +
clean-pyc: ## remove Python file artifacts
find . -name '*.pyc' -exec rm -f {} +
find . -name '*.pyo' -exec rm -f {} +
find . -name '*~' -exec rm -f {} +
find . -name '__pycache__' -exec rm -fr {} +
clean-test: ## remove test and coverage artifacts
rm -fr .tox/
rm -f .coverage
rm -f coverage.xml
rm -fr htmlcov/
rm -fr .pytest_cache
dist: clean ## builds source and wheel package
python -m build
ls -l dist
docs: clean-docs ## build documentation
echo "# CLI Help Documentation\n" > docs/docs/cli.md
@echo '```bash' >> docs/docs/cli.md
@echo "erdantic --help" >> docs/docs/cli.md
@echo '```' >> docs/docs/cli.md
@echo "" >> docs/docs/cli.md
@echo '```' >> docs/docs/cli.md
@erdantic --help >> docs/docs/cli.md
@echo '```' >> docs/docs/cli.md
sed 's|https://raw.githubusercontent.com/drivendataorg/erdantic/main/docs/docs/examples/pydantic.svg|examples/pydantic.svg|g' README.md \
| sed 's|https://erdantic.drivendata.org/stable/||g' \
> docs/docs/index.md
sed 's|https://erdantic.drivendata.org/stable/||g' HISTORY.md > docs/docs/changelog.md
rm -f docs/docs/examples/diagram.png
cd docs && mkdocs build
docs-notebooks:
rm -f docs/docs/examples/diagram.png
jupyter nbconvert --execute --clear-output docs/docs/examples/pydantic.ipynb
rm -f docs/docs/examples/diagram.png
jupyter nbconvert --execute --clear-output docs/docs/examples/dataclasses.ipynb
rm -f docs/docs/examples/diagram.png
format: ## format code with black
ruff format erdantic tests docs
ruff check --fix erdantic tests docs
help:
@python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST)
lint: ## run linting and code quality checks
ruff format --check erdantic tests docs
ruff check erdantic tests docs
pypitest: dist
twine upload --repository testpypi dist/*
requirements: ## install development requirements
pip install -r requirements-dev.txt
static-test-assets:
python tests/scripts/generate_static_assets.py
test: ## run tests
python -m pytest -vv
typecheck: ## run mypy typechecking
mypy --install-types --non-interactive erdantic
from pathlib import Path
import platform
import shutil
import nox
def find_uv() -> tuple[bool, str]:
# Inspired by:
# https://github.com/wntrblm/nox/blob/08813c3c6b0d2171c280bbfcf219d089a16d1ac2/nox/virtualenv.py#L42
uv = shutil.which("uv")
if uv is not None:
return True, uv
return False, "uv"
HAS_UV, UV = find_uv()
@nox.session(venv_backend="mamba|conda", python="3.11", reuse_venv=True)
def dev(session):
"""Set up a development environment."""
session.conda_install("graphviz", channel="conda-forge")
if platform.system() == "Windows":
session.conda_install("pygraphviz", channel="conda-forge")
if HAS_UV:
session.run(UV, "pip", "install", "-r", "requirements/dev.txt", external=True)
else:
session.install("-r", "requirements/dev.txt")
conda_cmd = session.virtualenv.conda_cmd
env_path = Path(session.virtualenv.location).relative_to(Path.cwd())
session.log(f"Activate with: {conda_cmd} activate {env_path}")
@nox.session(venv_backend="uv|virtualenv", python="3.11", reuse_venv=True)
def lint(session):
session.env.pop("CONDA_PREFIX", None) # uv errors if both venv and conda env are active
session.install("-r", "requirements/lint.txt")
session.run("ruff", "format", "--check")
session.run("ruff", "check")
@nox.session(venv_backend="mamba|conda", python="3.11", reuse_venv=True)
def typecheck(session):
session.conda_install("graphviz", channel="conda-forge")
if platform.system() == "Windows":
session.conda_install("pygraphviz", channel="conda-forge")
if HAS_UV:
session.run(UV, "pip", "install", "-r", "requirements/typecheck.txt", external=True)
else:
session.install("-r", "requirements/typecheck.txt")
session.run("mypy", "--install-types", "--non-interactive")
class CoverageCleaner:
"""Global coverage cleaner to clean up coverage artifacts once per nox invocation."""
def __init__(self):
self.been_run = False
def clean(self, session):
if not self.been_run:
session.log("Cleaning up coverage artifacts.")
self.been_run = True
Path(".coverage").unlink(missing_ok=True)
Path("coverage.xml").unlink(missing_ok=True)
shutil.rmtree("htmlcov", ignore_errors=True)
coverage_cleaner = CoverageCleaner()
@nox.session(
venv_backend="mamba|conda",
python=["3.8", "3.9", "3.10", "3.11", "3.12"],
reuse_venv=True,
)
def tests(session):
session.conda_install("graphviz", channel="conda-forge")
if platform.system() == "Windows":
session.conda_install("pygraphviz", channel="conda-forge")
if HAS_UV:
session.run(UV, "pip", "install", "-r", "requirements/tests.txt", external=True)
else:
session.install("-r", "requirements/tests.txt")
coverage_cleaner.clean(session)
session.run("pytest", "-vv")
@nox.session(venv_backend="uv|virtualenv", reuse_venv=True)
def build(session):
session.env.pop("CONDA_PREFIX", None) # uv errors if both venv and conda env are active
session.install("build")
session.run("python", "-m", "build")
@nox.session(venv_backend="mamba|conda", python="3.12", reuse_venv=False)
@nox.parametrize("extras", ["", "[attrs]"])
def test_wheel(session, extras):
session.conda_install("graphviz", channel="conda-forge")
if platform.system() == "Windows":
session.conda_install("pygraphviz", channel="conda-forge")
wheel_path = next(Path("dist").glob("*.whl")).resolve()
if HAS_UV:
session.run(UV, "pip", "install", f"erdantic{extras} @ {wheel_path}", external=True)
else:
session.install(str(wheel_path) + extras)
session.run("python", "-m", "erdantic", "--version")
session.run(
"python", "-c", "import erdantic; import erdantic.examples; print(erdantic.list_plugins())"
)
@nox.session(venv_backend="mamba|conda", python="3.12", reuse_venv=False)
def test_sdist(session):
session.conda_install("graphviz", channel="conda-forge")
if platform.system() == "Windows":
session.conda_install("pygraphviz", channel="conda-forge")
sdist_path = next(Path("dist").glob("*.tar.gz")).resolve()
if HAS_UV:
session.run(UV, "pip", "install", f"erdantic @ {sdist_path}", external=True)
else:
session.install(sdist_path)
session.run("python", "-m", "erdantic", "--version")
session.run(
"python", "-c", "import erdantic; import erdantic.examples; print(erdantic.list_plugins())"
)
def _docs_base(session):
session.conda_install("graphviz", channel="conda-forge")
if platform.system() == "Windows":
session.conda_install("pygraphviz", channel="conda-forge")
if HAS_UV:
session.run(UV, "pip", "install", "-r", "requirements/docs.txt", external=True)
else:
session.install("-r", "requirements/docs.txt")
examples_dir = Path("docs/docs/examples").resolve()
examples_dir.mkdir(exist_ok=True)
for notebook_path in sorted(Path("docs/notebooks").glob("*.ipynb")):
out_path = examples_dir / notebook_path.name
session.run("jupyter", "execute", f"--output={out_path}", notebook_path)
@nox.session(venv_backend="mamba|conda", python="3.11", reuse_venv=True)
def docs(session):
_docs_base(session)
with session.chdir("docs"):
session.run("mkdocs", "build")
@nox.session(venv_backend="mamba|conda", python="3.11", reuse_venv=True)
def docs_serve(session):
_docs_base(session)
with session.chdir("docs"):
session.run("mkdocs", "serve")
-e .[attrs]
-r lint.txt
ipython
ipykernel
-e .[attrs]
black
markdown-callouts>=0.4.0
mdx_truly_sane_lists==1.3
mike
mkdocs>=1.4
mkdocs-jupyter
mkdocs-material>=7.2.6
mkdocstrings[python]>=0.19.0
nbconvert>=7.7.0
rich
ruff>=0.1.14
-e .[attrs]
filetype
ipython
pytest<8
pytest-cases
pytest-cov
rich
attrs
mypy
pydantic>=2
sortedcontainers-pydantic
typenames
typer

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet