Smarter Annotations Library

Kubernetes-style annotations on every Smarter resource: their validation, their storage as rows of Annotation, queries and selectors, and the handlers and signal that integrations use to react when they change. See Annotations for how manifests use them.

Smarter annotations: Kubernetes-style key-value metadata on every Smarter resource.

Every resource built on smarter.lib.django.models.MetaDataModel has annotations, set from its manifest’s metadata.annotations. Integrations use them to associate Smarter resources with resources in their own systems, query them (selectors, models.Annotation), and react when they change (handlers, signals).

Validation

Validation and normalization of Smarter annotations.

Smarter annotations follow Kubernetes annotations: a map of string keys to string values that any client or extension may attach to a resource, for its own purposes. A key is an optional DNS-subdomain prefix that names its owner, e.g. acme.com, then a slash and a name, e.g. acme.com/crm-account-id. Smarter’s keys are a superset of Kubernetes keys: the name may have several slash-separated segments, e.g. smarter.sh/llmclient/use-case. Every valid Kubernetes key is a valid Smarter key.

The functions here are the one place that decides what a valid annotation is. The SAM manifest models, the MetaDataModel.annotations setter, and the data migrations all call them.

smarter.lib.annotation.validation.AnnotationsInput = None | str | dict[str, typing.Any] | list[dict[str, typing.Any]]

What callers may pass as annotations.

A map, the legacy list of one-key maps, either of them as a JSON string, or None.

smarter.lib.annotation.validation.AnnotationsMap

string keys mapped to string values, as in Kubernetes.

Type:

A resource’s annotations

alias of dict[str, str]

exception smarter.lib.annotation.validation.SmarterAnnotationError(message='')[source]

Bases: SmarterValueError

Raised for an invalid annotation key, value or set of annotations.

smarter.lib.annotation.validation.annotations_size(annotations)[source]

Return the size of a resource’s annotations in bytes: every key and value, UTF-8 encoded.

Parameters:

annotations (dict[str, str]) – The annotations.

Returns:

The size in bytes.

Return type:

int

smarter.lib.annotation.validation.coerce_annotation_value(key, value, lenient=False)[source]

Return an annotation’s value as a string.

Annotation values are strings, as in Kubernetes. YAML parses unquoted scalars such as true, 42 or 2026-10-28 as other types, so they are converted to the string that was written: booleans to "true" or "false", dates to ISO 8601, None to "".

Parameters:
  • key (str) – The annotation’s key, for the error message.

  • value (object) – The value.

  • lenient (bool) – Encode lists and dicts as JSON instead of rejecting them. Only data migrations of legacy rows set this.

Raises:

SmarterAnnotationError – If the value is a list, a dict, or bytes that aren’t UTF-8.

Returns:

The value as a string.

Return type:

str

smarter.lib.annotation.validation.is_reserved_annotation_key(key)[source]

Return whether a key belongs to the Smarter platform: its prefix is smarter.sh or a subdomain.

Parameters:

key (str) – The annotation key.

Returns:

True if the key’s prefix is reserved.

Return type:

bool

smarter.lib.annotation.validation.normalize_annotations(value, lenient=False)[source]

Return annotations in any accepted shape as a validated map of strings to strings.

Accepts the Kubernetes map, {"acme.com/crm-id": "42"}, and the list of one-key maps that older SAM manifests use, [{"acme.com/crm-id": "42"}], or either one as a JSON string.

Example:

normalize_annotations([{"acme.com/active": True}])  # {"acme.com/active": "true"}
Parameters:
  • value (None | str | dict[str, Any] | list[dict[str, Any]]) – The annotations.

  • lenient (bool) – For legacy rows in data migrations: skip invalid keys, keep the last value of a duplicate key, encode lists and dicts as JSON, and don’t enforce the size limit.

Raises:

SmarterAnnotationError – If a key or value is invalid, a key appears twice, or the annotations are larger than 256 KiB.

Returns:

The annotations, in their original order.

Return type:

dict[str, str]

smarter.lib.annotation.validation.prefix_matches(prefix, owner)[source]

Return whether a key’s prefix is owner or one of its subdomains.

Example:

prefix_matches("crm.acme.com", "acme.com")  # True
prefix_matches("notacme.com", "acme.com")   # False
Parameters:
Returns:

True if the prefix belongs to the owner.

Return type:

bool

smarter.lib.annotation.validation.split_annotation_key(key)[source]

Split an annotation key into its prefix and its name.

The prefix is everything before the first slash. A key without a slash has no prefix.

Example:

split_annotation_key("acme.com/crm/account-id")  # ("acme.com", "crm/account-id")
split_annotation_key("owner")                    # (None, "owner")
Parameters:

key (str) – The annotation key.

Returns:

The prefix, or None, and the name.

Return type:

tuple[str | None, str]

smarter.lib.annotation.validation.validate_annotation_key(key)[source]

Validate an annotation key, and return it.

A key is [prefix/]name. The prefix, when present, is a DNS subdomain of at most 253 characters. The name is one or more segments separated by slashes. Each segment is at most 63 characters, alphanumeric at both ends, with -, _ and . between. The whole key is at most 317 characters.

Parameters:

key (str) – The annotation key.

Raises:

SmarterAnnotationError – If the key is not valid.

Returns:

The key.

Return type:

str

Constants of Smarter annotations, which follow Kubernetes annotations.

smarter.lib.annotation.const.ANNOTATIONS_TOTAL_MAX_BYTES = 262144

256 KiB, as in Kubernetes.

Type:

The most that one resource’s annotations, keys and values together, may hold

smarter.lib.annotation.const.ANNOTATION_KEY_MAX_LENGTH = 317

the longest key that Kubernetes accepts.

Type:

The longest key, 317 characters

smarter.lib.annotation.const.ANNOTATION_KEY_PREFIX_MAX_LENGTH = 253

The longest key prefix, a DNS subdomain such as acme.com, as in Kubernetes.

smarter.lib.annotation.const.ANNOTATION_KEY_SEGMENT_MAX_LENGTH = 63

The longest segment of a key’s name, the parts between slashes, as in Kubernetes.

smarter.lib.annotation.const.ANNOTATION_SELECTOR_PARAM = 'annotation'

The query parameter of the cli get api that holds an annotation selector, e.g. ?annotation=acme.com/crm-id=42.

smarter.lib.annotation.const.RESERVED_ANNOTATION_PREFIXES = ('smarter.sh',)

Key prefixes that belong to the Smarter platform, as kubernetes.io belongs to Kubernetes.

A prefix’s subdomains are reserved too, e.g. llmclient.smarter.sh. Manifests may carry these keys, but only Smarter’s own code registers handlers for them.

('smarter.sh', )
smarter.lib.annotation.const.SOURCE_COMMIT_ANNOTATION = 'smarter.sh/source-commit'

The commit of SOURCE_REPOSITORY_ANNOTATION that the manifest was applied from.

smarter.lib.annotation.const.SOURCE_PATH_ANNOTATION = 'smarter.sh/source-path'

The manifest’s file, relative to the root of SOURCE_REPOSITORY_ANNOTATION, e.g. llm-clients/stackademy.yaml.

smarter.lib.annotation.const.SOURCE_REPOSITORY_ANNOTATION = 'smarter.sh/source-repository'

The repository that a resource was applied from, e.g. https://github.com/smarter-sh/stackademy.

manage.py load_from_github sets it, as Flux and Argo CD annotate the Kubernetes objects that they apply from git.

Model and Queries

The Annotation model: one row per annotation of a Smarter resource.

Annotations belong to smarter.lib.django.models.MetaDataModel, which is abstract, so a row points at its resource through Django’s content types framework, as django-taggit’s tags do: a content_type and an object_id. A resource that uses multi-table inheritance, e.g. a PluginMetaStatic, keeps its annotations under its root model, PluginMeta, the table that holds its metadata, so that both classes see the same annotations.

class smarter.lib.annotation.models.Annotation(*args, **kwargs)[source]

Bases: TimestampedModel

One annotation of a Smarter resource: a key and a string value, as in Kubernetes.

Read and write a resource’s annotations through smarter.lib.django.models.MetaDataModel.annotations. Use this model directly to query annotations across resources.

Example:

from smarter.lib.annotation.models import Annotation

# every Smarter resource of any kind that maps to CRM account A-10042
Annotation.objects.with_key("acme.com/crm-account-id", "A-10042").resources()

# every annotation that acme.com's integration owns
Annotation.objects.with_prefix("acme.com")
Parameters:
  • id (Unknown) – Primary key: ID

  • created_at (Unknown) – Created at

  • updated_at (Unknown) – Updated at

  • object_id (Unknown) – Object id. The primary key of the annotated resource, as a string: some resources’ keys are strings.

  • key (Unknown) – Key. The annotation key, e.g. ‘acme.com/crm-account-id’: an optional DNS prefix, then a name.

  • prefix (Unknown) – Prefix. The key’s prefix, e.g. ‘acme.com’, or empty when the key has none. Set from the key.

  • value (Unknown) – Value. The annotation value, a string.

  • content_object (Unknown) – Generic foreign key to the ContentType specified in content_type

Relationship fields:

Parameters:

content_type (Unknown) – Content type. The kind of the annotated resource: its root model. (related name: +)

exception DoesNotExist

Bases: ObjectDoesNotExist

exception MultipleObjectsReturned

Bases: MultipleObjectsReturned

exception NotUpdated

Bases: ObjectNotUpdated, DatabaseError

content_object

Provide a generic many-to-one relation through the content_type and object_id fields.

This class also doubles as an accessor to the related object (similar to ForwardManyToOneDescriptor) by adding itself as a model attribute.

content_type

ForeignKey to ContentType

Content type. The kind of the annotated resource: its root model. (related name: +)

Type:

Type

content_type_id

Internal field, use content_type instead.

created_at

DateTimeField

Created at

Timestamp indicating when the model instance was created.

This field is automatically set to the current date and time when the instance is first created. It is indexed in the database for efficient querying.

Type:

Type

id: int

BigAutoField

Primary key: ID

The row’s primary key, which Django adds to every concrete model. Declared, without a value, only so that type checkers know it: Django ignores a bare annotation.

Type:

Type

key

CharField

Key. The annotation key, e.g. ‘acme.com/crm-account-id’: an optional DNS prefix, then a name.

Type:

Type

object_id

CharField

Object id. The primary key of the annotated resource, as a string: some resources’ keys are strings.

Type:

Type

objects: AnnotationManager = <smarter.lib.annotation.models.AnnotationManager object>
prefix

CharField

Prefix. The key’s prefix, e.g. ‘acme.com’, or empty when the key has none. Set from the key.

Type:

Type

save(*args, **kwargs)[source]

Set the prefix from the key, then save.

updated_at

DateTimeField

Updated at

Timestamp indicating when the model instance was last updated.

This field is automatically updated to the current date and time whenever the instance is saved. It is indexed in the database for efficient querying.

Type:

Type

value

TextField

Value. The annotation value, a string.

Type:

Type

class smarter.lib.annotation.models.AnnotationManager(*args, **kwargs)[source]

Bases: Manager

The manager of Annotation, whose queries are AnnotationQuerySet.

It declares the queryset’s methods, so that type checkers know them on Annotation.objects.

all()[source]

Return every annotation.

Return type:

AnnotationQuerySet

delete_for_object(instance)[source]

See AnnotationQuerySet.delete_for_object().

Return type:

list[AnnotationChange]

exclude(*args, **kwargs)[source]

Return the annotations that don’t match the filter.

Return type:

AnnotationQuerySet

filter(*args, **kwargs)[source]

Return the annotations that match the filter.

Return type:

AnnotationQuerySet

for_model(model)[source]

See AnnotationQuerySet.for_model().

Return type:

AnnotationQuerySet

for_object(instance)[source]

See AnnotationQuerySet.for_object().

Return type:

AnnotationQuerySet

get_queryset()[source]

Return an AnnotationQuerySet of every annotation.

Return type:

AnnotationQuerySet

none()[source]

Return no annotations.

Return type:

AnnotationQuerySet

sync(instance, annotations)[source]

Make a resource’s annotations exactly annotations; see AnnotationQuerySet.sync().

Parameters:
  • instance (Model) – The resource, already saved.

  • annotations (dict[str, str]) – The resource’s annotations, already validated.

Return type:

list[AnnotationChange]

Returns:

The changes, in key order.

with_key(key, value=None)[source]

Return the annotations with a key, and optionally a value; see AnnotationQuerySet.with_key().

Parameters:
  • key (str) – The annotation key, e.g. acme.com/crm-account-id.

  • value (str | None) – The value to match exactly, or None for any value.

Return type:

AnnotationQuerySet

Returns:

The annotations.

with_prefix(prefix)[source]

See AnnotationQuerySet.with_prefix().

Return type:

AnnotationQuerySet

class smarter.lib.annotation.models.AnnotationQuerySet(model=None, query=None, using=None, hints=None)[source]

Bases: QuerySet

Queries of annotations, across one resource, one kind, or every kind.

delete_for_object(instance)[source]

Delete a resource’s annotations, when the resource itself is deleted.

Handlers and the annotations_changed signal are sent the removals, with resource_deleted=True.

Parameters:

instance (Model) – The resource being deleted.

Returns:

The removals.

Return type:

list[AnnotationChange]

for_model(model)[source]

Return the annotations of every resource of a model, e.g. every LLMClient.

Parameters:

model (type[Model] | Model) – A model class or instance.

Returns:

The annotations.

Return type:

AnnotationQuerySet

for_object(instance)[source]

Return the annotations of one resource.

Parameters:

instance (Model) – The resource.

Returns:

The annotations.

Return type:

AnnotationQuerySet

object_ids_for(model)[source]

Return the object_id of each annotation, as the primary key type of model, for a subquery.

object_id is a string, because some resources’ primary keys are. Casting it back to an integer key lets the database compare it with the resource table’s index.

Example:

LLMClient.objects.filter(pk__in=Annotation.objects.with_key("acme.com/id").object_ids_for(LLMClient))
Parameters:

model (type[Model] | Model) – The resources’ model.

Returns:

A single-column queryset of primary keys.

Return type:

QuerySet

resources()[source]

Return the resources, of any kind, that these annotations belong to.

This is the lookup from a key in another system to the Smarter resources that are associated with it, e.g. every resource annotated acme.com/crm-account-id=A-10042. Resources are returned as their root model (see annotation_root_model()), and are not filtered by any user’s permissions.

Example:

Annotation.objects.with_key("acme.com/crm-account-id", "A-10042").resources()
Returns:

The resources, ordered by kind, then primary key.

Return type:

list[Model]

sync(instance, annotations)[source]

Make a resource’s annotations exactly annotations, and return what changed.

Keys that aren’t in annotations are removed, as kubectl apply does with a manifest’s annotations. Registered annotation handlers, and the annotations_changed signal, are sent the changes once the transaction commits.

Parameters:
  • instance (Model) – The resource, already saved.

  • annotations (dict[str, str]) – The resource’s annotations, already validated.

Returns:

The changes, in key order.

Return type:

list[AnnotationChange]

with_key(key, value=None)[source]

Return the annotations with a key, and optionally a value.

Parameters:
  • key (str) – The annotation key, e.g. acme.com/crm-account-id.

  • value (str | None) – The value to match exactly, or None for any value.

Returns:

The annotations.

Return type:

AnnotationQuerySet

with_prefix(prefix)[source]

Return the annotations whose key’s prefix is prefix, or one of its subdomains.

Parameters:

prefix (str) – A DNS domain, e.g. acme.com.

Returns:

The annotations.

Return type:

AnnotationQuerySet

smarter.lib.annotation.models.annotation_content_type(model)[source]

Return the content type under which a resource’s annotations are stored.

Parameters:

model (type[Model] | Model) – A model class or instance.

Returns:

The content type of annotation_root_model().

Return type:

ContentType

smarter.lib.annotation.models.annotation_root_model(model)[source]

Return the model under which a resource’s annotations are stored.

That is the resource’s farthest concrete ancestor: the model itself, unless it uses multi-table inheritance. Proxy models resolve to their concrete model.

Parameters:

model (type[Model] | Model) – A model class or instance.

Returns:

The root model.

Return type:

type[Model]

smarter.lib.annotation.models.get_cached_annotations(content_type_id, pk)[source]

Return the annotations of one resource, from the cache when possible.

Invalidated, once the transaction commits, whenever one of the resource’s rows is saved or deleted, however that happens: by AnnotationQuerySet.sync(), by the Django admin, or directly (see smarter.lib.annotation.receivers).

Parameters:
  • content_type_id (int) – The id of the resource’s content type, from annotation_content_type().

  • pk (str) – The resource’s primary key, as a string.

Returns:

The annotations, in the order they were added.

Return type:

dict[str, str]

smarter.lib.annotation.models.prefetch_annotations(instances)[source]

Read the annotations of many resources at once, in one query per kind.

This is prefetch_related() for annotations, which aren’t a relation that Django can prefetch. A list that shows every resource’s annotations calls it with a page of resources, so that the page doesn’t cost one query per resource. Each instance then answers smarter.lib.django.models.MetaDataModel.annotations from what was read, until its annotations are next assigned or saved. Instances that have no annotations are skipped.

Example:

page = list(LLMClient.objects.filter(user_profile=user_profile)[:50])
prefetch_annotations(page)
[llmclient.annotations for llmclient in page]  # no more queries
Parameters:

instances (Iterable[Model]) – The resources, of one kind or several.

Return type:

None

Selectors

Annotation selectors: filter resources by their annotations.

The syntax is that of Kubernetes label selectors, one requirement at a time. Several requirements must all match. Values are free text and may contain commas, so a selector is a list of requirements, not one comma-separated string. See smarter.lib.annotation.contract.AnnotationRequirement.

Example:

from smarter.apps.llmclient.models import LLMClient
from smarter.lib.annotation.selectors import filter_by_annotations

filter_by_annotations(LLMClient.objects.all(), ["acme.com/crm-account-id=A-10042", "!acme.com/archived"])
smarter.lib.annotation.selectors.AnnotationSelector = None | str | collections.abc.Iterable[str | smarter.lib.annotation.contract.AnnotationRequirement]

One requirement, a list of them, or None for no requirement.

smarter.lib.annotation.selectors.filter_by_annotations(queryset, selector)[source]

Return the resources of queryset that match every requirement of selector.

As in Kubernetes, key!=value also matches resources that don’t have the key.

Parameters:
Raises:

SmarterAnnotationError – If a requirement is invalid.

Returns:

The filtered queryset.

Return type:

TypeVar(QuerySetType, bound= QuerySet)

smarter.lib.annotation.selectors.model_with_annotation(model, key, value=None)[source]

Return every resource of model that has the annotation key, with value if given.

Parameters:
  • model (type[Model]) – The model, e.g. LLMClient.

  • key (str) – The annotation key.

  • value (str | None) – The value to match, or None for any value.

Returns:

The resources.

Return type:

QuerySet

smarter.lib.annotation.selectors.parse_annotation_requirement(requirement)[source]

Parse one requirement, e.g. acme.com/crm-id=A-10042.

Parameters:

requirement (str) – key, !key, key=value, key==value or key!=value.

Raises:

SmarterAnnotationError – If the requirement or its key is invalid.

Returns:

The requirement.

Return type:

AnnotationRequirement

smarter.lib.annotation.selectors.parse_annotation_selector(selector)[source]

Parse a selector into its requirements.

Parameters:

selector (None | str | Iterable[str | AnnotationRequirement]) – One requirement, a list of them, or None.

Raises:

SmarterAnnotationError – If a requirement is invalid.

Returns:

The requirements, empty for None.

Return type:

list[AnnotationRequirement]

Handlers and Signals

Annotation handlers: code that reacts when annotations with a given prefix change.

A handler is the Smarter counterpart of a Kubernetes controller that watches an annotation. An integration owns a key prefix, e.g. acme.com, and registers a handler for it. Whenever a manifest is applied, or a resource deleted, in a way that adds, changes or removes an annotation whose prefix is acme.com or one of its subdomains, the handler is called once per changed annotation, after the database transaction commits.

Example:

# acme_integration/apps.py
from django.apps import AppConfig

class AcmeIntegrationConfig(AppConfig):
    name = "acme_integration"

    def ready(self):
        from . import handlers  # noqa: F401  registers the handlers below

# acme_integration/handlers.py
from smarter.lib.annotation.handlers import AnnotationEvent, annotation_handler
from .tasks import link_crm_account

@annotation_handler("acme.com")
def crm_account(event: AnnotationEvent) -> None:
    if event.change.key == "acme.com/crm-account-id" and event.change.action != "removed":
        link_crm_account.delay(event.model_label, event.object_id, event.change.new_value)

Handlers run in the request that applied the manifest. Hand slow work, such as calls to a legacy system, to a Celery task. An exception in a handler is logged, and doesn’t affect the resource or the other handlers.

class smarter.lib.annotation.handlers.AnnotationEvent(instance, model_label, object_id, change, resource_deleted=False)[source]

What an annotation handler is called with: one changed annotation of one resource.

__init__(instance, model_label, object_id, change, resource_deleted=False)
change: AnnotationChange

The annotation’s key, and its value before and after.

instance: Model

The resource.

After a deletion, it is the deleted instance, and its pk may be None.

model_label: str

The resource’s model, e.g. llmclient.LLMClient.

object_id: int | str

The resource’s primary key.

Unlike instance.pk, it survives the resource’s deletion.

resource_deleted: bool = False

True when the annotation was removed because the resource was deleted.

smarter.lib.annotation.handlers.AnnotationHandler

A function that is called with an AnnotationEvent.

alias of Callable[[AnnotationEvent], None]

smarter.lib.annotation.handlers.annotation_handler(prefix)[source]

Register the decorated function as an annotation handler for prefix.

See register_annotation_handler().

Parameters:

prefix (str) – A DNS domain that the integration owns, e.g. acme.com.

Returns:

The decorator.

Return type:

Callable[[Callable[[AnnotationEvent], None]], Callable[[AnnotationEvent], None]]

smarter.lib.annotation.handlers.dispatch_annotation_changes(instance, changes, resource_deleted=False)[source]

Send changes to the annotations_changed signal and the matching handlers, once the transaction commits.

Parameters:
  • instance (Model) – The resource.

  • changes (list[AnnotationChange]) – What changed.

  • resource_deleted (bool) – True when the resource was deleted.

Return type:

None

smarter.lib.annotation.handlers.handlers_for_key(key)[source]

Return the handlers to call when the annotation key changes.

Parameters:

key (str) – An annotation key.

Returns:

The handlers of every registered prefix that the key’s prefix matches.

Return type:

list[Callable[[AnnotationEvent], None]]

smarter.lib.annotation.handlers.register_annotation_handler(prefix, handler)[source]

Call handler whenever an annotation whose key’s prefix is prefix, or a subdomain of it, changes.

The prefixes that belong to the Smarter Project, smarter.sh and its subdomains, may only be handled by Smarter’s own code.

Parameters:
Raises:

SmarterAnnotationError – If the prefix isn’t a DNS subdomain, or is reserved.

Returns:

The handler, so that this can be used as a decorator.

Return type:

Callable[[AnnotationEvent], None]

smarter.lib.annotation.handlers.unregister_annotation_handler(prefix, handler)[source]

Stop calling handler for prefix.

Does nothing if it isn’t registered.

Parameters:
Return type:

None

Signals of Smarter annotations.

smarter.lib.annotation.signals.annotations_changed = <django.dispatch.dispatcher.Signal object>

Sent after a transaction that added, changed or removed a resource’s annotations commits.

Keyword arguments:

  • sender: the resource’s model class.

  • instance: the resource.

  • object_id: the resource’s primary key, which survives the resource’s deletion.

  • changes: a list of smarter.lib.annotation.contract.AnnotationChange.

  • resource_deleted: True when the annotations were removed because the resource was deleted.

Receivers that care about one integration’s keys should register an annotation handler instead (smarter.lib.annotation.handlers.annotation_handler()), which is called with that prefix’s changes only.

Django signal receivers of Smarter annotations.

smarter.lib.annotation.receivers.delete_annotations(sender, instance, **kwargs)[source]

Delete a resource’s annotations when the resource is deleted.

Annotations point at their resource through a content type, not a foreign key, so the database doesn’t cascade the deletion.

smarter.lib.annotation.receivers.invalidate_cached_annotations(sender, instance, **kwargs)[source]

Make a resource’s cached annotations stale when one of its rows is saved or deleted.

Rows change through sync(), and also through the Django admin and direct writes, which don’t go through it. Waiting for the commit keeps a request that reads the rows before the change commits from caching the old ones.

Pydantic models of annotation changes and selectors.

smarter.lib.annotation.contract.AnnotationAction

What happened to an annotation.

alias of Literal[‘added’, ‘changed’, ‘removed’]

class smarter.lib.annotation.contract.AnnotationChange(**data)[source]

Bases: BaseModel

One annotation of a resource that was added, changed or removed.

property action: Literal['added', 'changed', 'removed']

Return added, changed or removed.

key: str
model_config = {'extra': 'forbid', 'frozen': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

new_value: str | None
old_value: str | None
property prefix: str | None

Return the key’s prefix, e.g. acme.com, or None.

smarter.lib.annotation.contract.AnnotationOperator

How an AnnotationRequirement tests a resource’s annotation.

alias of Literal[‘exists’, ‘not_exists’, ‘equals’, ‘not_equals’]

class smarter.lib.annotation.contract.AnnotationRequirement(**data)[source]

Bases: BaseModel

One requirement of an annotation selector, in the syntax of Kubernetes label selectors.

Selector

Matches resources whose annotation

acme.com/crm-id

acme.com/crm-id exists, with any value

!acme.com/crm-id

acme.com/crm-id doesn’t exist

acme.com/crm-id=A-10042

acme.com/crm-id is A-10042

acme.com/crm-id!=A-10042

acme.com/crm-id is missing, or isn’t A-10042

key: str
model_config = {'extra': 'forbid', 'frozen': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

operator: Literal['exists', 'not_exists', 'equals', 'not_equals']
value: str | None

Data Migration

The data migration that moves annotations out of the JSON column of each model into Annotation rows.

MetaDataModel.annotations used to be a JSON column on every resource’s table, holding a list of one-key maps. Each app that owns such a table has an annotation_rows migration that copies the column into smarter.lib.annotation.models.Annotation rows, then removes it. This module is the copy, shared by those migrations.

Note

This is a one-time migration, for upgrading a platform from Smarter 0.37 to the latest stable release. Once every platform has upgraded, it has nothing left to do. Nothing outside those annotation_rows migrations should call it.

smarter.lib.annotation.data_migration.copy_annotations_to_rows(app_label, *model_names)[source]

Return the RunPython operation that copies the annotations column of models into Annotation rows.

Runs before the RemoveField operations of the same migration. Legacy values are normalized leniently: invalid keys are skipped, a repeated key keeps its last value, and list or dict values are kept as JSON strings. The reverse copies the rows back into the column, as a list of one-key maps.

Parameters:
  • app_label (str) – The app’s label, e.g. llmclient.

  • model_names (str) – The models whose table has the column, e.g. LLMClient.

Returns:

The operation.

Return type:

RunPython