Skip to content

Reference

cattle_grid.extensions

BaseConfig

Bases: BaseModel

Source code in cattle_grid/extensions/__init__.py
class BaseConfig(BaseModel): ...

Extension dataclass

Data model for an extension

Parameters:

Name Type Description Default
name str
required
module str
required
lifespan Callable[list, Awaitable[None]] | None
None
config_class Any
<class 'cattle_grid.extensions.BaseConfig'>
Config Any | None
None
configuration Any | None
None
activity_router RabbitRouter

Includable to RabbitBroker router.

<dynamic>
api_router APIRouter

APIRouter class, used to group path operations, for example to structure an app in multiple files. It would then be included in the FastAPI app, or in another APIRouter (ultimately included in the app).

Read more about it in the FastAPI docs for Bigger Applications - Multiple Files.

Example
from fastapi import APIRouter, FastAPI

app = FastAPI()
router = APIRouter()


@router.get("/users/", tags=["users"])
async def read_users():
    return [{"username": "Rick"}, {"username": "Morty"}]


app.include_router(router)
<dynamic>
api_prefix str
''
transformer Callable[list, Awaitable[Dict]] | None
None
transformer_inputs List[str] | None
None
transformer_outputs List[str] | None
None
lookup_method Callable[list, Awaitable[Dict]] | None
None
lookup_order int | None
None
method_information List[MethodInformation]

Built-in mutable sequence.

If no argument is given, the constructor creates a new empty list. The argument must be an iterable if specified.

<dynamic>
Source code in cattle_grid/extensions/__init__.py
@dataclass
class Extension:
    """Data model for an extension"""

    name: str
    """name of the extension, must be unique"""

    module: str
    """module the extension is defined in, should be set to `__name__`"""

    lifespan: Callable[[], Awaitable[None]] | None = None
    """The lifespan function"""

    config_class: Any = BaseConfig
    """Expected configuration class"""

    Config: Any | None = None
    """Annotation to retrieve the configuration, e.g.

    ```python
    @extension.lookup()
    async def lookup(
        lookup: Lookup, config: extension.Config
    ) -> Lookup: ...
    ```
    """
    configuration: Any | None = None

    activity_router: RabbitRouter = field(default_factory=RabbitRouter)
    api_router: APIRouter = field(default_factory=APIRouter)
    api_prefix: str = ""

    transformer: Callable[[Dict], Awaitable[Dict]] | None = None
    transformer_inputs: List[str] | None = None
    transformer_outputs: List[str] | None = None

    lookup_method: Callable[[str], Awaitable[Dict]] | None = None
    lookup_order: int | None = None

    method_information: List[MethodInformation] = field(default_factory=list)

    def __post_init__(self):
        def get_config():
            return self.configuration

        self.Config = Annotated[self.config_class, Depends(get_config)]

    def configure(self, config: dict):
        """Configures the extension

        The configuration is validated using the config_class.
        """
        self.configuration = self.config_class.model_validate(config)

    def transform(self, inputs: List[str] = [], outputs: List[str] = []):
        """Allows building the extension via decorator. Usage:

        ```python
        extension = Extension("my extension")

        @extension.transform(inputs=["inputs"], outputs=["outputs"])
        async def transformer(a: dict):
            ...
        ```
        """
        if self.transformer:
            raise ValueError("You should not override an existing transformer")

        def inner(func):
            self.transformer = func
            self.transformer_inputs = inputs
            self.transformer_outputs = outputs

            return func

        return inner

    def subscribe(self, routing_key: str, description: str | None = None):
        """Allows building the extension via decorator.

        ```python
        extension = Extension("my extension")

        @extension.subscribe("routing_key")
        async def subscriber(msg: dict): ...
        ```

        Dependency injection is available for the subscriber function.
        """

        def inner(func):
            if description is None:
                function_description = func.__doc__
            else:
                function_description = description

            self.activity_router.subscriber(
                routing_key, exchange=exchange(), title=routing_key
            )(func)

            if not skip_method_information(routing_key):

                self.method_information.append(
                    MethodInformation(
                        module=self.module,
                        routing_key=routing_key,
                        description=function_description,
                    )
                )

            return func

        return inner

    def lookup(self):
        """Allows building the extension via decorator.

        ```python
        extension = Extension("my extension")

        @extension.lookup()
        async def lookup(l: Lookup) -> Lookup:
            ...
        ```
        Dependency injection is available for the lookup function.
        """

        def inner(func):
            self.lookup_method = func
            return func

        return inner

    def get(self, path):
        """Allows one to add a get endpoint to the API Router
        of the extension

        """

        def inner(func):
            self.api_router.get(path)(func)
            return func

        return inner

Config = None class-attribute instance-attribute

Annotation to retrieve the configuration, e.g.

@extension.lookup()
async def lookup(
    lookup: Lookup, config: extension.Config
) -> Lookup: ...

config_class = BaseConfig class-attribute instance-attribute

Expected configuration class

lifespan = None class-attribute instance-attribute

The lifespan function

module instance-attribute

module the extension is defined in, should be set to __name__

name instance-attribute

name of the extension, must be unique

configure(config)

Configures the extension

The configuration is validated using the config_class.

Source code in cattle_grid/extensions/__init__.py
def configure(self, config: dict):
    """Configures the extension

    The configuration is validated using the config_class.
    """
    self.configuration = self.config_class.model_validate(config)

get(path)

Allows one to add a get endpoint to the API Router of the extension

Source code in cattle_grid/extensions/__init__.py
def get(self, path):
    """Allows one to add a get endpoint to the API Router
    of the extension

    """

    def inner(func):
        self.api_router.get(path)(func)
        return func

    return inner

lookup()

Allows building the extension via decorator.

extension = Extension("my extension")

@extension.lookup()
async def lookup(l: Lookup) -> Lookup:
    ...
Dependency injection is available for the lookup function.

Source code in cattle_grid/extensions/__init__.py
def lookup(self):
    """Allows building the extension via decorator.

    ```python
    extension = Extension("my extension")

    @extension.lookup()
    async def lookup(l: Lookup) -> Lookup:
        ...
    ```
    Dependency injection is available for the lookup function.
    """

    def inner(func):
        self.lookup_method = func
        return func

    return inner

subscribe(routing_key, description=None)

Allows building the extension via decorator.

extension = Extension("my extension")

@extension.subscribe("routing_key")
async def subscriber(msg: dict): ...

Dependency injection is available for the subscriber function.

Source code in cattle_grid/extensions/__init__.py
def subscribe(self, routing_key: str, description: str | None = None):
    """Allows building the extension via decorator.

    ```python
    extension = Extension("my extension")

    @extension.subscribe("routing_key")
    async def subscriber(msg: dict): ...
    ```

    Dependency injection is available for the subscriber function.
    """

    def inner(func):
        if description is None:
            function_description = func.__doc__
        else:
            function_description = description

        self.activity_router.subscriber(
            routing_key, exchange=exchange(), title=routing_key
        )(func)

        if not skip_method_information(routing_key):

            self.method_information.append(
                MethodInformation(
                    module=self.module,
                    routing_key=routing_key,
                    description=function_description,
                )
            )

        return func

    return inner

transform(inputs=[], outputs=[])

Allows building the extension via decorator. Usage:

extension = Extension("my extension")

@extension.transform(inputs=["inputs"], outputs=["outputs"])
async def transformer(a: dict):
    ...
Source code in cattle_grid/extensions/__init__.py
def transform(self, inputs: List[str] = [], outputs: List[str] = []):
    """Allows building the extension via decorator. Usage:

    ```python
    extension = Extension("my extension")

    @extension.transform(inputs=["inputs"], outputs=["outputs"])
    async def transformer(a: dict):
        ...
    ```
    """
    if self.transformer:
        raise ValueError("You should not override an existing transformer")

    def inner(func):
        self.transformer = func
        self.transformer_inputs = inputs
        self.transformer_outputs = outputs

        return func

    return inner

cattle_grid.extensions.load

add_routers_to_broker(broker, extensions)

Adds the routers to the broker

Source code in cattle_grid/extensions/load/__init__.py
def add_routers_to_broker(broker: RabbitBroker, extensions: List[Extension]):
    """Adds the routers to the broker"""

    for extension in extensions:
        if extension.activity_router:
            broker.include_router(extension.activity_router)

add_routes_to_api(app, extensions)

Adds the routes to the api

Source code in cattle_grid/extensions/load/__init__.py
def add_routes_to_api(app: FastAPI, extensions: List[Extension]):
    """Adds the routes to the api"""
    for extension in extensions:
        if extension.api_router:
            if extension.api_prefix:
                app.include_router(extension.api_router, prefix=extension.api_prefix)

build_lookup(extensions)

Builds the lookup method

Source code in cattle_grid/extensions/load/__init__.py
def build_lookup(extensions: List[Extension]) -> LookupMethod:
    """Builds the lookup method"""
    methods = ordered_lookups(extensions)

    async def lookup_result(lookup: Lookup) -> Lookup:
        for method in methods:
            lookup = await inject(method)(lookup)
            if lookup.result is not None:
                return lookup
        return lookup

    return lookup_result

build_transformer(extensions)

Build the transformer

Source code in cattle_grid/extensions/load/__init__.py
def build_transformer(extensions: List[Extension]) -> Callable[[Dict], Awaitable[Dict]]:
    """Build the transformer"""
    transformers = get_transformers(extensions)
    steps = transformation_steps(transformers)

    async def transformer(data: dict):
        for step in steps:
            for plugin in step:
                data.update(await plugin.transformer(data))

        return data

    return transformer

collect_method_information(extensions)

Collects the method information from the extensions

Source code in cattle_grid/extensions/load/__init__.py
def collect_method_information(extensions: List[Extension]) -> List[Dict]:
    """Collects the method information from the extensions"""
    return sum((extension.method_information for extension in extensions), [])

lifespan_from_extensions(extensions) async

Creates the lifespan from the extensions

Source code in cattle_grid/extensions/load/__init__.py
@asynccontextmanager
async def lifespan_from_extensions(extensions: List[Extension]):
    """Creates the lifespan from the extensions"""
    lifespans = collect_lifespans(extensions)

    async with iterate_lifespans(lifespans):
        yield

load_extension(extension_information)

Loads a single extension

Source code in cattle_grid/extensions/load/__init__.py
def load_extension(extension_information: dict) -> Extension:
    """Loads a single extension"""
    module_name = extension_information.get("module")

    if module_name is None:
        raise ValueError("module is required")

    module = importlib.import_module(module_name)
    extension = module.extension
    extension.configure(extension_information.get("config", {}))

    if "lookup_order" in extension_information:
        extension.lookup_order = extension_information["lookup_order"]
    if "api_prefix" in extension_information:
        extension.api_prefix = extension_information["api_prefix"]

    return extension

load_extensions(settings)

Loads the extensions from settings

Source code in cattle_grid/extensions/load/__init__.py
def load_extensions(settings) -> List[Extension]:
    """Loads the extensions from settings"""

    extensions = [
        load_extension(extension_information)
        for extension_information in settings.extensions
    ]

    logger.info("Loaded extensions: %s", ", ".join(f"'{e.name}'" for e in extensions))

    return extensions

set_globals(extensions)

Sets global variables in cattle_grid.dependencies

Source code in cattle_grid/extensions/load/__init__.py
def set_globals(extensions: List[Extension]):
    """Sets global variables in cattle_grid.dependencies"""
    import cattle_grid.dependencies.globals

    cattle_grid.dependencies.globals.transformer = build_transformer(extensions)
    cattle_grid.dependencies.globals.lookup = build_lookup(extensions)