Requests and Views

As housekeeping, we remove the override part from the previous step.

Web frameworks, and even systems like pytest, model individual units of work as a “request”. The request contains the information about the thing being worked on (the URL, the headers) along with other information unique to the operation. When finished, the request is thrown away.

Let’s borrow that idea and that jargon. Each Customer coming through the door is part of a Request.

Let’s also steal another idea from web frameworks and use a View to collect all the information needed for generating a result, plus the logic to generate that result.

  • “interaction” -> Request

  • View manages the generation of the output

  • Datastore is now a dict-like thing to get individual Customers

  • The Url is stored in the container, so anything that wants to get the currently-processed URL, can ask the container for it

  • Request is created from a request factory with the URL of the Customer and also stores the container (in case a View wants to do a lookup)

  • A View factory which gets the Request, Context, and Greeter from the container, then renders the greeting

Code

__init__.py

"""

A customer walks into a store. Do the steps to interact with them:

- Get a correct greeter

- Interact with them

Simple wired application:

- Settings that say what punctuation to use

- Registry

- A bundled Greeter and Customer

- An add-on which defines a FrenchGreeter and FrenchCustomer

- A Datastore which stores/retrieves instances of Customers

- Request and View factories to assemble the full processing chain

"""

from typing import List

from wired import ServiceRegistry

from .models import Customer, Datastore, Greeter, Request, Resource, Settings, Url, View


def app_bootstrap(settings: Settings) -> ServiceRegistry:
    # Make the registry
    registry = ServiceRegistry()

    # Do setup for the core application features
    setup(registry, settings)

    # Import the add-on and initialize it
    from .custom import setup as addon_setup

    addon_setup(registry, settings)

    return registry


def setup(registry: ServiceRegistry, settings: Settings):
    """Initialize the features in the core application"""

    # Make and register the Datastore singleton
    datastore = Datastore()
    registry.register_singleton(datastore, Datastore)

    # Context factory
    def context_factory(container) -> Resource:
        # Presumes that "url" is in the container
        ds: Datastore = container.get(Datastore)
        url: str = container.get(Url)
        context: Resource = ds.customers.get(url)
        return context

    registry.register_factory(context_factory, Resource)

    # Request factory
    def request_factory(container) -> Request:
        url: str = container.get(Url)
        request = Request(url=url, container=container)
        return request

    registry.register_factory(request_factory, Request)

    # **** Default View
    def view_factory(container) -> View:
        request: Request = container.get(Request)
        context: Resource = container.get(Resource)
        greeter: Greeter = container.get(Greeter, context=context)
        view = View(request=request, context=context, greeter=greeter)
        return view

    registry.register_factory(view_factory, View)

    # **** Default Greeter
    def default_greeter_factory(container) -> Greeter:
        # Use the dataclass default for greeting
        return Greeter(punctuation=settings.punctuation)

    # Register it as a factory using its class for the "key"
    registry.register_factory(default_greeter_factory, Greeter)

    # During bootstrap, make some Customers
    mary = Customer(name='mary', title='Mary')
    datastore.customers['mary'] = mary


def process_request(registry: ServiceRegistry, url: str) -> str:
    """Given URL (customer name), make a Request to handle interaction"""

    # Make the container that this request gets processed in
    container = registry.create_container()

    # Put the url into the container
    container.register_singleton(url, Url)

    # Create a View to generate the greeting
    view = container.get(View)

    # Generate a response
    response = view()

    return response


def sample_interactions(registry: ServiceRegistry) -> List[str]:
    """Pretend to do a couple of customer interactions"""

    return [process_request(registry, url) for url in ('mary', 'henri')]


def main():
    settings = Settings(punctuation='!!')
    registry = app_bootstrap(settings)
    greetings = sample_interactions(registry)
    assert greetings == ['Hello Mary !!', 'Bonjour Henri !!']

custom.py

"""

A custom add-on to our app which adds FrenchCustomer and
French Greeter.

"""

from dataclasses import dataclass

from wired import ServiceContainer, ServiceRegistry

from .models import Customer, Datastore, Greeter, Settings


@dataclass
class FrenchCustomer(Customer):
    pass


@dataclass
class FrenchGreeter(Greeter):
    greeting: str = 'Bonjour'


def setup(registry: ServiceRegistry, settings: Settings):
    # The French greeter, using context of FrenchCustomer
    punctuation = settings.punctuation

    def french_greeter_factory(container) -> Greeter:
        return FrenchGreeter(punctuation=punctuation)

    # Register it as a factory using its class for the "key", but
    # this time register with a "context"
    registry.register_factory(french_greeter_factory, Greeter, context=FrenchCustomer)

    # Grab the Datastore and add a FrenchCustomer
    container: ServiceContainer = registry.create_container()
    datastore: Datastore = container.get(Datastore)
    henri = FrenchCustomer(name='henri', title='Henri')
    datastore.customers['henri'] = henri

models.py

"""

Models used in the core application.

Putting models in their own file is considered good practice for
code clarity. It also solves the problem of potential circular
imports.

"""

from dataclasses import dataclass, field
from typing import Dict

from wired import ServiceContainer


@dataclass
class Url:
    value: str


@dataclass
class Resource:
    name: str
    title: str


@dataclass
class Customer(Resource):
    pass


@dataclass
class Datastore:
    customers: Dict[str, Customer] = field(default_factory=dict)


@dataclass
class Settings:
    punctuation: str


@dataclass
class Request:
    url: str
    container: ServiceContainer


@dataclass
class Greeter:
    punctuation: str
    greeting: str = 'Hello'


@dataclass
class View:
    request: Request
    context: Resource
    greeter: Greeter

    def __call__(self) -> str:
        g = self.greeter
        return f'{g.greeting} {self.context.title} {g.punctuation}'