Metadata-Version: 2.4
Name: fractal-core
Version: 1.0.0
Summary: The bottom layer of the Fractal stack: settings, exceptions, a service base class and the utilities every layer above reaches for.
Keywords: settings,exceptions,ddd,clean architecture
Author-email: Douwe van der Meij <douwe@karibu-online.nl>
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Libraries :: Python Modules
License-File: LICENSE
Requires-Dist: python-dotenv
Requires-Dist: pytest>=7.0.0 ; extra == "dev"
Requires-Dist: pytest-cov>=4.0.0 ; extra == "dev"
Requires-Dist: black>=23.0.0 ; extra == "dev"
Requires-Dist: ruff>=0.1.0 ; extra == "dev"
Requires-Dist: mypy>=1.0.0 ; extra == "dev"
Requires-Dist: isort>=5.13.2 ; extra == "dev"
Project-URL: Documentation, https://github.com/Fractal-Forge/fractal-core#readme
Project-URL: Homepage, https://github.com/Fractal-Forge/fractal-core
Project-URL: Issues, https://github.com/Fractal-Forge/fractal-core/issues
Project-URL: Repository, https://github.com/Fractal-Forge/fractal-core
Provides-Extra: dev

# Fractal Core

> Fractal Core is the bottom layer of the Fractal stack: settings, exceptions, a service base class, and the utilities every layer above reaches for.

[![PyPI Version][pypi-image]][pypi-url]
[![Build Status][build-image]][build-url]

<!-- Badges -->

[pypi-image]: https://img.shields.io/pypi/v/fractal-core
[pypi-url]: https://pypi.org/project/fractal-core/
[build-image]: https://github.com/Fractal-Forge/fractal-core/actions/workflows/build.yml/badge.svg
[build-url]: https://github.com/Fractal-Forge/fractal-core/actions/workflows/build.yml

## Installation

```sh
pip install fractal-core
```

## Background

Every layer of an application needs a few of the same things: somewhere to read
configuration, a way to say a rule was broken, a shape for the adapters the
application installs. Left unowned, those get re-invented per layer, or they end
up in whichever module happened to need them first — and then that module
becomes a dependency of everything.

This package owns them instead, and it owns nothing else. **Nothing here
imports another Fractal library.** That constraint is the point: it is what
lets [fractal-commands](https://github.com/Fractal-Forge/fractal-commands),
events, processes and the application context all build on this without
importing each other.

## Settings

`Settings` is a singleton, loaded once from the environment:

```python
import os

from fractal_core import Settings


class AppSettings(Settings):
    ROOT_DIR = "/srv/app"          # optional: where to look for .env

    def load(self):
        self.DEBUG = os.getenv("DEBUG", "false") == "true"
        self.DATABASE_URL = os.getenv("DATABASE_URL")


settings = AppSettings()           # loads .env, then calls load()
AppSettings() is settings          # True — same instance from here on
```

Reading the environment exactly once is deliberate: configuration that can
change under a running process is configuration you cannot reason about. Use
`settings.reload({...})` when you do want to replace it, typically in tests.

`get_parameters` reads several at once and fails on the first one missing,
naming it:

```python
url, debug = settings.get_parameters(["DATABASE_URL", "DEBUG"])
# FractalException: Settings does not provide 'DATABSE_URL'
```

## Exceptions

Two hierarchies, kept apart on purpose:

- **`DomainException`** — a business rule was violated. It carries the shape an
  HTTP layer needs (`code`, `status_code`, `payload`, `headers`) so the domain
  can stay ignorant of HTTP while still saying enough to be rendered. Ships with
  `ObjectNotFoundException` (404) and `AggregateRootError` (405).
- **`FractalException`** — the application was assembled wrong: a setting that
  was never defined, a context missing a service. Not something to hand a
  caller.

The split matters at the edge. An HTTP layer can map every `DomainException`
onto a response and let anything else become a 500, which is exactly right:
a broken rule is an answer, a broken wiring is a bug.

## Services

```python
from fractal_core import Service


class Mailer(Service):
    @classmethod
    def install(cls, context):
        mailer = cls()
        yield mailer
        mailer.close()          # anything after the yield runs on teardown

    def is_healthy(self) -> bool:
        return self.connection.ping()
```

`install` is a generator so an adapter can hold a resource open for the
lifetime of the application. `is_healthy` is what the application context
checks on startup.

## Utilities

```python
from fractal_core import (
    EnhancedEncoder,     # json.JSONEncoder that also handles datetime, UUID, Decimal, set, objects
    all_subclasses,      # transitive subclasses of a class
    camel_to_snake, snake_to_camel, camel_case_to_spaces,
    slugify, slug_is_valid,
    init_logging,
)

json.dumps({"at": datetime.now(), "id": uuid4()}, cls=EnhancedEncoder)
```

`EnhancedEncoder` falls back to an object's `__dict__`, which is broad but not
universal — something without one still raises `TypeError` rather than encoding
as nothing.

## Development

```sh
make dev-install
make test
make lint
make format
```

