Skip to content

Commit

Permalink
Re-brand while we can
Browse files Browse the repository at this point in the history
  • Loading branch information
hynek committed Jul 24, 2023
1 parent b5439c0 commit 2cff4c4
Show file tree
Hide file tree
Showing 16 changed files with 133 additions and 123 deletions.
2 changes: 1 addition & 1 deletion .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Contributing Guide

Please, for now, only contribute in the form of [feedback](https://github.com/hynek/svc-reg/discussions/1) and/or [issues](https://github.com/hynek/svc-reg/issues).
Please, for now, only contribute in the form of [feedback](https://github.com/hynek/svcs/discussions/1) and/or [issues](https://github.com/hynek/svcs/issues).

The project is not ready for code contributions yet.
Yes, I still use `git push --force` occasionally.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,8 @@ jobs:
python-version-file: .python-version-default

- run: python -Im pip install -e .[dev]
- run: python -Ic 'import svc_reg'
- run: python -Ic 'import svc_reg.flask'
- run: python -Ic 'import svcs'
- run: python -Ic 'import svcs.flask'

# Ensure everything required is passing for branch protection.
required-checks-pass:
Expand Down
18 changes: 9 additions & 9 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ The **first number** of the version is the year.
The **second number** is incremented with each release, starting at 1 for each year.
The **third number** is for emergencies when we need to start branches for older releases.

You can find our backwards-compatibility policy [here](https://github.com/hynek/svc-reg/blob/main/.github/SECURITY.md).
You can find our backwards-compatibility policy [here](https://github.com/hynek/svcs/blob/main/.github/SECURITY.md).

<!-- changelog follows -->


## [Unreleased](https://github.com/hynek/svc-reg/compare/23.3.0...HEAD)
## [Unreleased](https://github.com/hynek/svcs/compare/23.3.0...HEAD)


## [23.3.0](https://github.com/hynek/svc-reg/compare/23.2.0...23.3.0) - 2023-07-20
## [23.3.0](https://github.com/hynek/svcs/compare/23.2.0...23.3.0) - 2023-07-20

### Added

Expand All @@ -25,29 +25,29 @@ You can find our backwards-compatibility policy [here](https://github.com/hynek/
It works with sync factories too, so you can use it universally in async code.
- Async method `ServicePing.aping()`.
It works with sync factories and pings too, so you can use it universally in async code.
[#4](https://github.com/hynek/svc-reg/pull/4)
[#4](https://github.com/hynek/svcs/pull/4)


### Changed

- Switched the cleanup mechanism from passing a function to allowing the factory to be a generator that yields the resource and can clean up after the `yield`.
Just like Pytest fixtures.
[#3](https://github.com/hynek/svc-reg/pull/3)
[#3](https://github.com/hynek/svcs/pull/3)


## [23.2.0](https://github.com/hynek/svc-reg/compare/23.1.0...23.2.0) - 2023-07-13
## [23.2.0](https://github.com/hynek/svcs/compare/23.1.0...23.2.0) - 2023-07-13

### Changed

- `Container.cleanup()` and `Container.acleanup` have been renamed to `close()` and `aclose()` respectively.
- The clean up methods are now more resilient by catching and logging exceptions at `warning` level.
That means that if the first clean up method fails, the second one will still be called.
- `svc_reg.flask.register_(factory|value)` now take the current Flask application as first argument.
The old behavior moved to `svc_reg.flask.replace_(factory|value)`.
- `svcs.flask.register_(factory|value)` now take the current Flask application as first argument.
The old behavior moved to `svcs.flask.replace_(factory|value)`.

The former requires no application context (and thusly be used in `init_app()`-style initializers) while the latter *does* require an application context and can be used to "monkey-patch" an existing application in tests.


## [23.1.0](https://github.com/hynek/svc-reg/tree/23.1.0) - 2023-07-12
## [23.1.0](https://github.com/hynek/svcs/tree/23.1.0) - 2023-07-12

- Initial release.
61 changes: 35 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,34 @@
<!-- begin-pypi -->

# A Service Locator for Python
<h1>
<p align="center">
<a href="https://github.com/hynek/svcs/">
<img src="docs/_static/logo.svg" width="25%" alt="svcs" />
</a>
</p>
</h1>


## A Service Locator for Python

> **Warning**
> ☠️ Not ready yet! ☠️
>
> This project is only public to [gather feedback](https://github.com/hynek/svc-reg/discussions), and everything can and will change until the project is proclaimed stable.
> This project is only public to [gather feedback](https://github.com/hynek/svcs/discussions), and everything can and will change until the project is proclaimed stable.
>
> Currently only [**Flask** support](#flask) 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.
*svc-reg* is a [service locator](https://en.wikipedia.org/wiki/Service_locator_pattern) for Python.
*svcs* (pronounced *services*) is a [service locator](https://en.wikipedia.org/wiki/Service_locator_pattern) 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 resources in *one central place* and access them all in a *consistent* way.**

---

In practice that means that at runtime, you say "*Give me a database connection*!", and *svc-reg* will give you whatever you've configured it to return when asked for a database connection.
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.

If you like the [*Dependency Inversion Principle*](https://en.wikipedia.org/wiki/Dependency_inversion_principle) (aka "*program against interfaces, not implementations*"), you would register concrete factories for abstract interfaces; in Python usually a [`Protocol`](https://docs.python.org/3/library/typing.html#typing.Protocol) or an [Abstract Base Class](https://docs.python.org/3.11/library/abc.html).
Expand Down Expand Up @@ -63,15 +72,15 @@ def engine_factory():
with engine.connect() as conn:
yield conn

registry = svc_reg.Registry()
registry = svcs.Registry()
registry.register_factory(Connection, engine_factory)
```

The generator-based setup and cleanup may remind you of [Pytest fixtures](https://docs.pytest.org/en/stable/explanation/fixtures.html).

Unlike typical dependency injection that passes your dependencies as arguments, the active obtainment of resources by calling `get()` when you *know* 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 are never used.

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

<!-- end-pypi -->

Expand All @@ -80,7 +89,7 @@ Unlike typical dependency injection that passes your dependencies as arguments,

You're unlikely to use the core API directly, but knowing what's happening underneath is good to dispel any concerns about magic.

*svc-reg* has two essential concepts:
*svcs* has two essential concepts:


### Registries
Expand All @@ -92,10 +101,10 @@ Its only job is to store and retrieve factories.
It is possible to register either factory callables or values:

```python
>>> import svc_reg
>>> import svcs
>>> import uuid

>>> reg = svc_reg.Registry()
>>> reg = svcs.Registry()

>>> reg.register_factory(uuid.UUID, uuid.uuid4)
>>> reg.register_value(str, "Hello World")
Expand All @@ -111,7 +120,7 @@ But the types must be *hashable* because they're used as keys in a lookup dictio
A **`Container`** belongs to a Registry and allows to create instances of the registered types, taking care of their life-cycle:

```python
>>> container = svc_reg.Container(reg)
>>> container = svcs.Container(reg)

>>> u = container.get(uuid.UUID)
>>> u
Expand Down Expand Up @@ -172,27 +181,27 @@ On the other hand, the `Container` object should live on a request-scoped object

## Flask

*svc-reg* has grown from my frustration with the repetitiveness of using the `get_x` that creates an `x` and then stores it on the `g` object [pattern](https://flask.palletsprojects.com/en/latest/appcontext/#storing-data).
*svcs* has grown from my frustration with the repetitiveness of using the `get_x` that creates an `x` and then stores it on the `g` object [pattern](https://flask.palletsprojects.com/en/latest/appcontext/#storing-data).

Therefore it comes with Flask support out of the box in the form of the `svc_reg.flask` module.
Therefore it comes with Flask support out of the box in the form of the `svcs.flask` module.
It:

- puts the registry into `app.config["svc_registry"]`,
- puts the registry into `app.config["svcsistry"]`,
- unifies the putting and caching of services on the `g` object by putting a container into `g.svc_container`,
- transparently retrieves them from there for you,
- and installs a [`teardown_appcontext()`](http://flask.pocoo.org/docs/latest/api#flask.Flask.teardown_appcontext) handler that calls `close()` on the container when a request is done.

---

You can add support for *svc-reg* by calling `svc_reg.flask.init_app(app)` in your [*application factory*](https://flask.palletsprojects.com/en/latest/patterns/appfactories/).
You can add support for *svcs* by calling `svcs.flask.init_app(app)` in your [*application factory*](https://flask.palletsprojects.com/en/latest/patterns/appfactories/).
For instance, to create a factory that uses a SQLAlchemy engine to produce connections, you could do this:

```python
from flask import Flask
from sqlalchemy import Connection, create_engine
from sqlalchemy.sql import text

import svc_reg
import svcs


def create_app(config_filename):
Expand All @@ -202,7 +211,7 @@ def create_app(config_filename):

##########################################################################
# Set up the registry using Flask integration.
app = svc_reg.flask.init_app(app)
app = svcs.flask.init_app(app)

# Now, register a factory that calls `engine.connect()` if you ask for a
# `Connection`. Since we use yield inside of a context manager, the
Expand All @@ -215,21 +224,21 @@ def create_app(config_filename):
yield conn

ping = text("SELECT 1")
svc_reg_flask.register_factory(
svcs_flask.register_factory(
# The app argument makes it good for custom init_app() functions.
app,
Connection,
engine_factory,
ping=lambda conn: conn.execute(ping)
)

# You also use svc_reg WITHIN factories:
svc_reg_flask.register_factory(
# You also use svcs WITHIN factories:
svcs_flask.register_factory(
app, # <---
AbstractRepository,
# No cleanup, so we just return an object using a lambda
lambda: Repository.from_connection(
svc_reg.flask.get(Connection)
svcs.flask.get(Connection)
),
)
##########################################################################
Expand All @@ -244,7 +253,7 @@ Now you can request the `Connection` object in your views:
```python
@app.get("/")
def index() -> flask.ResponseValue:
conn: Connection = svc_reg.flask.get(Connection)
conn: Connection = svcs.flask.get(Connection)
```

If you have a [health endpoint](https://kubernetes.io/docs/reference/using-api/health-checks/), it could look like this:
Expand All @@ -259,7 +268,7 @@ def healthy() -> flask.ResponseValue:
failing: list[dict[str, str]] = []
code = 200

for svc in svc_reg.flask.get_pings():
for svc in svcs.flask.get_pings():
try:
svc.ping()
ok.append(svc.name)
Expand Down Expand Up @@ -312,7 +321,7 @@ In practice, you can simplify/beautify the code within your views by creating a
Say this is `app/services.py`:

```python
from svc_reg.flask import (
from svcs.flask import (
get,
get_pings,
init_app,
Expand Down Expand Up @@ -373,19 +382,19 @@ If types are more important to you than a unified interface, you can always wrap

```python
def get_connection() -> Connection:
return svc_reg.flask.get(Connection)
return svcs.flask.get(Connection)
```

Or, if you don't care about `Protocols`:

```python
def get(svc_type: type[T]) -> T:
return svc_reg.flask.get(svc_type)
return svcs.flask.get(svc_type)
```


## Credits

*svc-reg* is written by [Hynek Schlawack](https://hynek.me/) and distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license.
*svcs* is written by [Hynek Schlawack](https://hynek.me/) and distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license.

The development is kindly supported by my employer [Variomedia AG](https://www.variomedia.de/) and all my amazing [GitHub Sponsors](https://github.com/sponsors/hynek).
6 changes: 3 additions & 3 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from sybil import Sybil
from sybil.parsers.rest import DocTestParser, PythonCodeBlockParser

import svc_reg
import svcs


pytest_collect_file = Sybil(
Expand All @@ -23,9 +23,9 @@

@pytest.fixture(name="registry")
def _registry():
return svc_reg.Registry()
return svcs.Registry()


@pytest.fixture(name="container")
def _container(registry):
return svc_reg.Container(registry)
return svcs.Container(registry)
1 change: 1 addition & 0 deletions docs/_static/logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 8 additions & 8 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ build-backend = "hatchling.build"

[project]
dynamic = ["version", "readme"]
name = "svc-reg"
name = "svcs"
description = "A Service Locator for Python"
requires-python = ">=3.8"
license = "MIT"
Expand All @@ -26,12 +26,12 @@ dependencies = ["attrs"]
[project.optional-dependencies]
tests = ["pytest", "pytest-asyncio", "sybil"]
typing = ["mypy>=1.4", "flask"]
dev = ["svc-reg[tests,typing]", "tox>4", "flask"]
dev = ["svcs[tests,typing]", "tox>4", "flask"]

[project.urls]
Changelog = "https://github.com/hynek/svc-reg/blob/main/CHANGELOG.md"
Documentation = "https://github.com/hynek/svc-reg/blob/main/README.md"
Source = "https://github.com/hynek/svc-reg"
Changelog = "https://github.com/hynek/svcs/blob/main/CHANGELOG.md"
Documentation = "https://github.com/hynek/svcs/blob/main/README.md"
Source = "https://github.com/hynek/svcs"
Funding = "https://github.com/sponsors/hynek"


Expand All @@ -52,7 +52,7 @@ text = """
---
For now, please refer to the [GitHub README](https://github.com/hynek/svc-reg/blob/main/README.md) for latest documentation.
For now, please refer to the [GitHub README](https://github.com/hynek/svcs/blob/main/README.md) for latest documentation.
## Release Information
Expand All @@ -68,7 +68,7 @@ pattern = "\n(###.+?\n)## "
text = """
---
[→ Full Changelog](https://github.com/hynek/svc-reg/blob/main/CHANGELOG.md)
[→ Full Changelog](https://github.com/hynek/svcs/blob/main/CHANGELOG.md)
"""
Expand All @@ -87,7 +87,7 @@ filterwarnings = ["once::Warning"]
[tool.coverage.run]
branch = true
parallel = true
source = ["svc_reg"]
source = ["svcs"]

[tool.coverage.paths]
source = ["src", ".tox/py*/**/site-packages"]
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
8 changes: 4 additions & 4 deletions src/svc_reg/flask.py → src/svcs/flask.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def init_app(app: Flask, registry: Registry | None = None) -> Flask:
if registry is None:
registry = Registry()

app.config["svc_registry"] = registry
app.config["svcsistry"] = registry
app.teardown_appcontext(teardown)

return app
Expand All @@ -40,7 +40,7 @@ def register_factory(
*,
ping: Callable | None = None,
) -> None:
app.config["svc_registry"].register_factory(svc_type, factory, ping=ping)
app.config["svcsistry"].register_factory(svc_type, factory, ping=ping)


def register_value(
Expand All @@ -50,7 +50,7 @@ def register_value(
*,
ping: Callable | None = None,
) -> None:
app.config["svc_registry"].register_value(svc_type, instance, ping=ping)
app.config["svcsistry"].register_value(svc_type, instance, ping=ping)


def replace_factory(
Expand Down Expand Up @@ -95,7 +95,7 @@ def teardown(exc: BaseException | None) -> None:


def _ensure_req_data() -> tuple[Registry, Container]:
registry: Registry = current_app.config["svc_registry"]
registry: Registry = current_app.config["svcsistry"]
if "svc_container" not in g:
g.svc_container = Container(registry)

Expand Down
File renamed without changes.
Loading

0 comments on commit 2cff4c4

Please sign in to comment.