Skip to main content

ether

GitHub Repo stars PyPI - Version PyPI - Python Version Coverage

ether - External Python Utilities for Improved Development.


Table of Contents

Installation

pip install ether-utils

Usage

This package offers a range of utilities across multiple categories, including data manipulation, services, and datetime.

  • service decorator
  • nget function - nested get
  • destruct function - to extract values from a dictionary, matching variable names from the caller's scope or from provided list
  • utc_now function - returns the current UTC time as a timezone-aware datetime object.

@service

service provides several instruments to write better services - @service decorator, Break exception and @catch_a_break decorator.

  • @service

A class decorator that behaves like @dataclass but also logs init arguments.

This is useful for debugging or tracing how services are constructed.

Args:

cls: The class to be decorated

Returns:

The decorated class with dataclass features and logging

Example:

@service
class UserService:
    user_id: str
    update_cache: bool = True

    def run(self):
        # Service implementation
        pass
  • @catch_a_break

Decorator that gracefully handles Break exceptions in service operations.

Catches any Break exceptions raised during the execution of the decorated function, logs the provided reason (or a default message if none is provided), and returns None to indicate the operation was terminated early.

Args:

func: The function to be decorated

Returns:

The decorated function that handles Break exceptions

Example:

@service
class DataProcessor:
    @catch_a_break
    def process(self, data):
        if not data:
            raise Break("Empty data provided")
        # Continue processing...

JUSTIFICATION OF NEED:

There was a time when Django developers wrote business logic in views or even in templates.
This was a poor practice, so the community found a better place for it — the model.
This approach was called **"thin views, fat models."**
While it works for small projects, even in medium-sized projects,
models quickly become **God objects**, making maintenance difficult.

To address this, developers adopted a better approach:
moving business logic into services, which can be either functions or classes.
This method enables the creation of reliable scenarios for use cases,
allowing them to be used in views, Celery tasks, or even in the shell.

Several packages provide syntactic sugar for services,
but sometimes we need something really simple.
That's where `et.services` comes in.
Let's consider the following example and refactor it using `et`.

Before:

DB = {}
class ServiceError(Exception):
    ...

@dataclass
class UserDTO:
    name: str
    age: int
    email: str

class UpdateUserService:
    user_dto: UserDTO

    def __init__(self, usr_dto: UserDTO):
        self.user_dto = usr_dto

    def run(self):
        log.info("Start creating user service")
        log.info("User DTO: %s", self.user_dto)

        if self.user_dto.email not in DB:
            log.info("User doesn't exists")
            raise ServiceError("User doesn't exists")

        user = DB[self.user_dto.email]

        if self.user_dto.name == user.name:
            log.info("User name is the same")
            return

        # update user logic

After:

DB = {}
class ServiceError(Exception):
    ...

@dataclass
class UserDTO:
    name: str
    age: int
    email: str

@service
class UpdateUserSvc:
    user_dto: UserDTO

    @catch_a_break
    def run(self):
        user = self.get_user()
        self.check_name(user)
        self.update_user(user)

    def get_user(self) -> UserDTO:
        if self.user_dto.email not in DB:
            log.info("User doesn't exists")
            raise ServiceError("User doesn't exists")

        return DB[self.user_dto.email]

    def check_name(self, user: UserDTO) -> None:
        if self.user_dto.name == user.name:
            # log.info("User name is the same") <- no need - `@catch_a_break` will log
            raise Break("User name has the same")

    def update_user(self, user: UserDTO) -> UserDTO:
        ...

Advantages of this approach:

  • No __init__ method – while simple in this case, some scenarios involve processing many arguments, making this approach more flexible.
  • Clear semantics with the @service decorator – it explicitly marks the class as a service, preventing confusion with @dataclass, which should be used for DTOs.
  • Automatic logging – service initialization logs arguments (args and kwargs) automatically.
  • Cleaner code structure – the run method contains only instructions, while business logic is encapsulated within separate methods.
  • Graceful error handling with structured logs – operations can be interrupted cleanly with meaningful log messages.

nget()

Retrieves a nested item from a dictionary, safely handling exceptions and returning None if any step fails. Useful for accessing data from a JSON.

Args:

dct: The dictionary to traverse.
items: A sequence of keys or indices to follow in the dictionary.
default: The default value to return if any key/index is not found.

Returns:

The value found at the end of the item chain, or None/default if any key/index is not found.

Example:

>>> data = {'result': {'users': [{'address': {'street': 'Main St'}}]}}
>>> nget(data, 'result', 'users', 0, 'address', 'street')
'Main St'
>>> nget(data, 'result.users.0.address.street')
'Main St'
>>> nget(data, 'result', 'users', 0, 'address', 'zipcode')
None
>>> nget(data, 'result', 'users', 0, 'address', 'zipcode', default='NY')
'NY'

destruct()

Mimics JavaScript's object destructuring. Extract values from a dictionary, matching variable names from the caller's scope.

This function inspects the calling frame and tries to match variable names that exist in the caller's code context with keys in the provided dictionary. It then returns a tuple of values from the dictionary based on those variable names.

Args:

dct: The dictionary to extract values from.
keys: Optional sequence of keys to extract. If None, keys are inferred from the assignment statement.
default: Default value to use when a key is not found in the dictionary. If not provided, KeyError will be raised for missing keys.

Returns:

Single value or a tuple of values from the dictionary corresponding
to the caller's variable names.

Raises:

KeyError: If any variable name from the caller is not found in the dictionary
    and default value is provided.
DestructError: If the function cannot complete successfully.

WARNING:

`destruct` relies on inspecting the caller's frame, which may not work properly
in interactive environments like the Python shell or Jupyter notebooks.
Use `keys` argument if you need to work in the shell.

Example:

person_dict = {"name": "John", "age": 30, "city": "New York"}

# Basic usage
name, age, city = destruct(person_dict)

# With default value for missing keys
name, age, country = destruct(person_dict, default="N/A")

# With explicit keys and default
name, country = destruct(person_dict, keys=["name", "country"], default="N/A")

utc_now()

Returns the current UTC time as a timezone-aware datetime object.

IMPLEMENTATION NOTE:

This function is defined separately to allow easy mocking in tests.
It delegates the call to `DateTimeProvider.utc_now()` but can be overridden
using fixtures to control datetime values in unit tests.

Example:

>>> utc_now()
datetime.datetime(2025, 5, 9, 17, 45, 40, 566021, tzinfo=datetime.timezone.utc)

Additionally, et provides a pytest fixture, mocked_now, which offers an in-memory implementation of utc_now(), enhancing test performance by eliminating unnecessary system clock access.

Example:

from datetime import UTC, datetime
from unittest.mock import Mock
from et import utc_now

class TestMockedNow:
    def test_mocked_now(self, mocked_now: Mock):
        returned_dt = mocked_now()

        assert utc_now() == returned_dt

    def test_with_provided_datetime(self, mocked_now: Mock):
        fixed_dt = datetime(2025, 1, 1, 12, 0, 0, tzinfo=UTC)
        mocked_now.return_value = fixed_dt

        assert utc_now() == fixed_dt

JUSTIFICATION OF NEED:

There are two advantages for using `utc_now()`.
First, it provides a convenient shortcut for retrieving the current datetime
in the UTC timezone.
Additionally, it can be easily replaced throughout the project if needed.

Second, it allows us to efficiently mock the current time for testing purposes.
Several packages provide functionality for this, including `freezegun`
and `time-machine`.
While `time-machine` is faster than `freezegun`, the `mocked_now` pytest fixture
offers even better performance in tests.

License

et is distributed under the terms of the MIT license.

Development

  • Installation
# Clone the repository
git clone https://github.com/tavor118/ether
cd ether

# Set up a virtual environment (optional but recommended)
python -m venv .venv
source .venv/bin/activate

# Install the package
pip install hatch
pip install -e .[dev]
  • Run tests
hatch test
hatch test --cover  # with coverage check
hatch test --cover --all  # all python versions

For more information, check hatch documentation: https://hatch.pypa.io/latest/tutorials/testing/overview/

  • Linting / formatting

Using pre-commit

pre-commit install
pre-commit run
pre-commit run --all-files

Using ruff

# run ruff
uv run ruff check .

# run ruff and fix
uv run ruff check --fix .

# format code using ruff
uv run ruff format .
  • Contribution flow
# 1. Branch off main
git checkout main && git pull
git checkout -b fix/<short-name>

# 2. Make changes, then auto-fix and format
uv run ruff check --fix .
uv run ruff format .

# 3. If the change should be released, bump the version on the same branch
hatch version            # check current version
hatch version patch      # or: minor / major
git commit -am "Bump version to $(hatch version)"

# 4. Verify locally
hatch test                    # or: hatch test --cover
pre-commit run --all-files

# 5. Commit, push, open a PR against `main`
git push -u origin fix/<short-name>
gh pr create --base main

CI runs tests and linting on the PR. Once green, merge to main.

  • Release flow

Releases to PyPI are automated by the Publish to PyPI GitHub Action, which triggers when a PR is merged into the release branch. The action reads the version from hatch version, creates a vX.Y.Z tag, builds the distribution, publishes to PyPI, and cuts a GitHub Release.

The version bump itself happens on the feature/fix branch (see step 3 of the contribution flow), so releasing is just promoting main to release:

gh pr create --base release --head main --title "Release v$(hatch version)"
# Merging the PR triggers the workflow: tag + build + publish + GitHub Release.

Metadata

Release files for ether-utils 0.2.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for ether-utils 0.2.1
File Size Uploaded Provenance
ether_utils-0.2.1.tar.gz 99.3 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for ether-utils 0.2.1
File Interpreter ABI Platform
ether_utils-0.2.1-py3-none-any.whl Python 3 none any Details

Total release size: 111.8 kB

Release files / ether_utils-0.2.1.tar.gz

Download URL ether_utils-0.2.1.tar.gz
Size 99.3 kB
Tags Source
SHA-256 checksum
How to use checksums
2302613a5868662a065953e442c3b65406794cbe9fd8f0067fa6cd170e57ca8a
BLAKE2b-256 checksum
How to use checksums
2f62ee50f5fc729393df273afb452a455bf7432c0bf3be37133d9d6e17e1f3d0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.12.8

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on May 13, 2026.

Transparency log

Release files / ether_utils-0.2.1-py3-none-any.whl

Download URL ether_utils-0.2.1-py3-none-any.whl
Size 12.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
fafb131484e664b3d1aca9e778594389cbea41f212a613d56f708f20289a92c9
BLAKE2b-256 checksum
How to use checksums
11b9e7c26008631bf29b9e3410a34998ad7a6453167fb80467e09fe2fd2f08f9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.12.8

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on May 13, 2026.

Transparency log

Security for ether-utils 0.2.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for ether_utils-0.2.1.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on May 13, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page