Skip to main content

A Lightweight Service Locator

Project description

svcs logo showing a radar

A Lightweight Service Locator for Python.

WARNING ☠️ Not ready yet! ☠️

This project is only public to gather feedback, and everything can and will change until the project is proclaimed stable.

Currently only Flask support is production-ready, but API details can still change.

At this point, it's unclear whether this project will become a "proper Hynek project". I will keep using it for my work projects, but whether this will grow beyond my personal needs depends on community interest.

svcs (pronounced services) is a service locator for Python. It provides you with a central place to register factories for types/interfaces and then imperatively request instances of those types with automatic cleanup and health checks.


This allows you to configure and manage all your resources in one central place, access them in a consistent way without worrying about cleaning them up and achieve loose coupling.


In practice that means that at runtime, you say "Give me a database connection!", and svcs will give you whatever you've configured it to return when asked for a database connection. This can be an actual database connection or it can be a mock object for testing. All of this happens within your application – service locators are not related to service discovery.

If you follow the Dependency Inversion Principle (aka "program against interfaces, not implementations"), you would register concrete factories for abstract interfaces; in Python usually a Protocol or an abstract base class.

If you follow the Hexagonal Architecture (aka "ports and adapters"), the registered types are ports and the factories produce the adapters. svcs gives you a well-defined way to make your application pluggable.

Benefits:

  • Eliminates tons of repetitive boilerplate code,
  • unifies acquisition and cleanups of resources,
  • simplifies testing through loose coupling,
  • and allows for easy health checks across all resources.

The goal is to minimize your business code to:

def view(request):
    db = request.svcs.get(Database)
    api = request.svcs.get(WebAPIClient)

With proper typing: db has the type Database and api has the type WebAPIClient to the type checker.

You can also ask for multiple services at once with the same typing benefits:

def view(request):
    db, api = request.svcs.get(Database, WebAPIClient)

Or, if you don't shy away from some global state and your web framework supports it, even:

def view():
    db, api = svcs.flask.get(Database, WebAPIClient)

You set it up like this:

import atexit

from sqlalchemy import Connection, create_engine

...

engine = create_engine("postgresql://localhost")

def connection_factory():
    with engine.connect() as conn:
        yield conn

registry = svcs.Registry()
registry.register_factory(
    Connection,
    connection_factory,
    on_registry_close=engine.dispose
)

@atexit.register
def cleanup():
    registry.close()  # calls engine.dispose()

The generator-based setup and cleanup may remind you of pytest fixtures. The hooks that are defined as on_registry_close are called when you call Registry.close() – e.g. when your application is shutting down.

svcs comes with full async support via a-prefixed methods (i.e. aget() instead of get(), et cetera).

IMPORTANT All of this may look over-engineered if you have only one or two resources. However, it starts paying dividends very fast once you go past that.

Is this Dependency Injection!?

No.

Although the concepts are related and share the idea of having a central registry of services, the ways they provide those services are fundamentally different: Dependency injection always passes your dependencies as arguments while you actively ask a service locator for them when you need them. That usually requires less opaque magic since nothing meddles with your function/method definitions. But you can use, e.g., your web framework's injection capabilities to inject the locator object into your views and benefit from svcs's upsides without giving up some of DI's ones.

The active acquisition of resources by calling get() when you know for sure you're going to need it avoids the conundrum of either having to pass a factory (e.g., a connection pool – which also puts the onus of cleanup on you) or eagerly creating resources that you never use:

def view(request):
    if request.form.valid():
        # Form is valid; only NOW get a DB connection
        # and pass it into your business logic.
        return handle_form_data(
            request.services.get(Database),
            form.data,
        )

    raise InvalidFormError()

The main downside is that it's impossible to verify whether all required dependencies have been configured without running the code.

If you still prefer dependency injection, check out incant.


For now, please refer to the GitHub README for latest documentation.

Release Information

Changed

  • Container.get() and Container.aget() now have type hints that only work with concrete classes but allow for type checking without repeating yourself. If you want to use abstract classes like typing.Protocol or ABCs, you can use Container.get_abstract() and Container.aget_abstract() instead.

Added

  • Container.get_abstract() and Container.aget_abstract(). They behave like Container.get() and Container.aget() before.

  • It is now possible to check if a service type is registered with a Registry by using in.

  • It is now possible to check if a service type has a cached instance within a Container by using in.

  • Registry and Container are now also an (async) context managers that call close() / aclose() on exit automatically.


→ Full Changelog

Credits

svcs is written by Hynek Schlawack and distributed under the terms of the MIT license.

The development is kindly supported by my employer Variomedia AG and all my amazing GitHub Sponsors.

The Bestagon locator logo is made by Lynn Root, based on an Font Awesome Icon.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

svcs-23.9.0.tar.gz (110.9 kB view details)

Uploaded Source

Built Distribution

svcs-23.9.0-py3-none-any.whl (11.3 kB view details)

Uploaded Python 3

File details

Details for the file svcs-23.9.0.tar.gz.

File metadata

  • Download URL: svcs-23.9.0.tar.gz
  • Upload date:
  • Size: 110.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/4.0.2 CPython/3.11.4

File hashes

Hashes for svcs-23.9.0.tar.gz
Algorithm Hash digest
SHA256 5b4c6f3a2cb2bd73a043044d7ecfc519affe3edc3307661d5b1a9ecff2cc2f17
MD5 666b0eedda0bd028471ed1e4a9ef21ec
BLAKE2b-256 bd51928d3ef4f9074496205c1c58a0ff2cd3370595770da6eb35298bba03100a

See more details on using hashes here.

File details

Details for the file svcs-23.9.0-py3-none-any.whl.

File metadata

  • Download URL: svcs-23.9.0-py3-none-any.whl
  • Upload date:
  • Size: 11.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/4.0.2 CPython/3.11.4

File hashes

Hashes for svcs-23.9.0-py3-none-any.whl
Algorithm Hash digest
SHA256 1ac9eb50b0d90480a011a3965cb7de40440a54fa28e0e2bfe812be8295111636
MD5 b68ffa07ee613b072dc0865f48146952
BLAKE2b-256 40f649ca2eb8b38839a89c4bb118579b564e03f580d979c39f5b1434340db8df

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page