ansys / ansys/pydynamicreporting
Add a supported public registry for serverless item and template types
- Dominant language
- Python
- Stars
- 12
- Forks
- 5
- Avg merge
- 3d 14h
- Merged PRs (30d)
- 9
Description
### 📝 Description of the feature
Add a supported public registry API for the concrete serverless item and template types.
PyDynamicReporting already maintains the authoritative runtime mappings needed for polymorphic creation and database hydration:
```python
class Item(BaseModel):
_type_registry = {}
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
Item._type_registry[cls.type] = cls
class Template(BaseModel):
_type_registry = {}
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
Template._type_registry[cls.report_type] = cls
```
However, both mappings are private and mutable. External applications that need to discover supported types must currently do one or more of the following:
- maintain duplicate enums or string lists;
- scan `ansys.dynamicreporting.core.serverless.__all__` and inspect classes;
- access `_type_registry` directly;
- use `inspect.getattr_static()` to locate the concrete `content` validator descriptor;
- read private `_properties` tuples;
- infer whether `ALLOWED_EXT = None` means “not file-backed” or “generic file accepting any extension.”
That creates avoidable coupling to implementation details and makes it difficult for downstream libraries, CLIs, schema generators, and protocol adapters to stay synchronized with the installed PyDynamicReporting version.
The public registry should be the single supported source for:
- resolving a discriminator to its concrete Python class;
- listing every supported concrete item, layout, and generator type;
- distinguishing abstract/sentinel entries from constructible types;
- reporting subclass-specific editable properties;
- identifying file-backed item types and their allowed extensions;
- providing concise descriptions suitable for documentation and generated schemas.
The empty `ReportType.DEFAULT` and `ItemType.NONE` sentinels must not be advertised as constructible types.
### 💡 Steps for implementing the feature
#### 1. Define immutable public descriptors
Add a small public module such as `serverless/registry.py`:
```python
from dataclasses import dataclass
from types import MappingProxyType
from typing import Literal, Mapping
@dataclass(frozen=True, slots=True)
class ItemTypeDefinition:
type: str
item_class: type[Item]
class_name: str
description: str
properties: tuple[str, ...]
file_backed: bool
allowed_extensions: tuple[str, ...] | None
@dataclass(frozen=True, slots=True)
class TemplateTypeDefinition:
report_type: str
template_class: type[Template]
class_name: str
description: str
kind: Literal["layout", "generator"]
properties: tuple[str, ...]
```
`file_backed` disambiguates these two valid meanings of `allowed_extensions=None`:
- a non-file item has no extension concept;
- generic `File` is file-backed and accepts any extension.
Keep the Python class in the descriptor so callers can pass the resolved class directly to `ADR.create_item()` or `ADR.create_template()`. The other fields remain JSON-serializable for schema/documentation adapters.
#### 2. Expose public listing and resolution functions
Provide stable functions or equivalent class methods:
```python
def get_item_type_registry() -> Mapping[str, ItemTypeDefinition]:
"""Return an immutable mapping keyed by the item type discriminator."""
def get_template_type_registry() -> Mapping[str, TemplateTypeDefinition]:
"""Return an immutable mapping keyed by the template report type."""
def resolve_item_type(type_name: str) -> type[Item]:
"""Resolve a supported concrete item type or raise a product exception."""
def resolve_template_type(report_type: str) -> type[Template]:
"""Resolve a supported concrete template type or raise a product exception."""
```
Return `MappingProxyType` or a defensive copy so consumers cannot mutate PyDynamicReporting's dispatch state:
```python
return MappingProxyType(dict(_ITEM_TYPE_DEFINITIONS))
```
Use one product-owned exception type for unknown discriminators and include the sorted supported values in its detail.
#### 3. Centralize metadata extraction inside PyDynamicReporting
Build the descriptors from the same class registration path used for dispatch. Do not maintain a second handwritten list.
For file metadata, resolve the static descriptor without invoking it:
```python
content_validator = inspect.getattr_static(item_class, "content")
file_backed = isinstance(content_validator, FileValidator)
allowed_extensions = (
None
if not file_backed or content_validator.ALLOWED_EXT is None
else tuple(content_validator.ALLOWED_EXT)
)
```
Normalize extensions to lowercase without a leading dot and guarantee deterministic ordering.
For properties, expose a public class-level property such as `editable_properties` instead of requiring consumers to read `_properties`:
```python
@classmethod
def editable_properties(cls) -> tuple[str, ...]:
return tuple(cls._properties)
```
Descriptions can default to the first non-empty line of the concrete class docstring, with an optional explicit class attribute for a more user-facing description.
#### 4. Make registration integrity explicit
The current `__init_subclass__` implementation silently overwrites an existing entry if two subclasses use the same discriminator. Reject duplicates during class registration unless the class object is identical:
```python
existing = Item._type_registry.get(cls.type)
if existing is not None and existing is not cls:
raise ValueError(
f"Duplicate item type {cls.type!r}: {existing.__name__} and {cls.__name__}"
)
```
Apply the same rule to template report types. Explicitly exclude abstract classes and empty sentinels from the public registry.
#### 5. Export and document the API
Add the descriptors and functions to `ansys.dynamicreporting.core.serverless.__all__`, document examples, and state that the returned mappings reflect the installed PyDynamicReporting package:
```python
from ansys.dynamicreporting.core.serverless import (
get_item_type_registry,
resolve_item_type,
)
for type_name, definition in get_item_type_registry().items():
print(type_name, definition.allowed_extensions)
image_class = resolve_item_type("image")
item = adr.create_item(image_class, name="Result", content="result.png")
```
#### 6. Add registry integrity tests
Tests should verify:
1. all eight concrete item types are present and `none` is absent;
2. every concrete layout and generator exported publicly is present and the empty default is absent;
3. keys equal each descriptor's discriminator;
4. every resolved class is a concrete subclass of `Item` or `Template`;
5. `Image`, `Animation`, and `Scene` expose their exact allowed extensions;
6. generic `File` reports `file_backed=True` and `allowed_extensions=None`;
7. non-file items report `file_backed=False`;
8. returned mappings and frozen descriptors cannot mutate dispatch state;
9. duplicate discriminator registration fails immediately;
10. listing and resolution use the same underlying registry.
#### Acceptance criteria
- Consumers no longer need `_type_registry`, `_properties`, export scanning, or descriptor introspection.
- Listing and runtime resolution are generated from one product-owned source.
- The registry contains only concrete constructible types.
- Item metadata distinguishes unrestricted files from non-file content.
- Duplicate discriminators cannot silently replace an existing type.
- The API is documented, typed, deterministic, immutable to callers, and covered by tests.
### 🔗 Useful links and references
- [Private item registry and subclass registration](https://github.com/ansys/pydynamicreporting/blob/d98b45adad663d015d4e17ad93dcd3749f2f3d81/src/ansys/dynamicreporting/core/serverless/item.py#L479-L521)
- [File validators and `ALLOWED_EXT`](https://github.com/ansys/pydynamicreporting/blob/d98b45adad663d015d4e17ad93dcd3749f2f3d81/src/ansys/dynamicreporting/core/serverless/item.py#L250-L327)
- [Private template registry and subclass registration](https://github.com/ansys/pydynamicreporting/blob/d98b45adad663d015d4e17ad93dcd3749f2f3d81/src/ansys/dynamicreporting/core/serverless/template.py#L93-L154)
- [Current public serverless exports](https://github.com/ansys/pydynamicreporting/blob/d98b45adad663d015d4e17ad93dcd3749f2f3d81/src/ansys/dynamicreporting/core/serverless/__init__.py#L23-L103)
- Related but non-duplicate umbrella issue: #278 concerns a common server/serverless interface; it does not define a supported type-metadata or resolution registry.
An open-and-closed issue audit found no existing issue defining this public registry contract.
Contributor guide
Assessment
This issue has not been assessed yet.