Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions eventsourcingdb/__init__.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,26 @@
from .bound import Bound, BoundType
from .client import Client
from .container import Container
from .errors import ClientError, CustomError, InternalError, ServerError, ValidationError
from .errors import (
ClientError,
CustomError,
InternalError,
ServerError,
ValidationError,
)
from .event import Event, EventCandidate
from .observe_events import (
IfEventIsMissingDuringObserve,
ObserveEventsOptions,
ObserveFromLatestEvent,
)
from .read_event_types import EventType
from .read_events import IfEventIsMissingDuringRead, Order, ReadEventsOptions, ReadFromLatestEvent
from .read_events import (
IfEventIsMissingDuringRead,
Order,
ReadEventsOptions,
ReadFromLatestEvent,
)
from .write_events import (
IsEventQlQueryTrue,
IsSubjectOnEventId,
Expand Down
29 changes: 12 additions & 17 deletions eventsourcingdb/client.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,23 @@
import json
from collections import OrderedDict
from collections.abc import AsyncGenerator

from types import TracebackType
from typing import Any, TypeAlias, TypeVar

from http import HTTPStatus
import json
from types import TracebackType
from typing import Any, Self, TypeAlias, TypeVar

from .errors import CustomError, InternalError, ServerError, ValidationError
from .event import Event, EventCandidate
from .http_client import HttpClient, Response
from .is_event import is_event
from .is_heartbeat import is_heartbeat
from .is_stream_error import is_stream_error
from .is_event import is_event
from .is_valid_server_header import is_valid_server_header
from .parse_raw_message import parse_raw_message
from .read_events import ReadEventsOptions

from .errors import CustomError, InternalError, ServerError, ValidationError
from .event import Event, EventCandidate
from .observe_events import ObserveEventsOptions
from .parse_raw_message import parse_raw_message
from .read_event_types import EventType, is_event_type
from .read_events import ReadEventsOptions
from .read_subjects import is_subject

from .write_events import Precondition
from .http_client import HttpClient, Response


T = TypeVar('T')

Expand All @@ -35,21 +30,21 @@
SubjectStream: TypeAlias = AsyncGenerator[str, None]


class Client():
class Client:
def __init__(
self,
base_url: str,
api_token: str,
) -> None:
self.__http_client = HttpClient(base_url=base_url, api_token=api_token)

async def __aenter__(self) -> 'Client':
async def __aenter__(self) -> Self:
await self.__http_client.__aenter__()
return self

async def __aexit__(
self,
exc_type: BaseException | None = None,
exc_type: type[BaseException] | None = None,
exc_val: BaseException | None = None,
exc_tb: TracebackType | None = None,
) -> None:
Expand Down
22 changes: 12 additions & 10 deletions eventsourcingdb/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@

import docker
import requests
from docker import DockerClient, errors
from cryptography.hazmat.primitives.asymmetric import ed25519
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import ed25519
from docker import DockerClient, errors

from .client import Client

logger = logging.getLogger(__name__)


class Container:
def __init__(
Expand All @@ -33,19 +35,19 @@ def _cleanup_existing_containers(self) -> None:
filters={"ancestor": f"{self._image_name}:{self._image_tag}"}
)
except errors.APIError as e:
logging.warning("Warning: Error listing existing containers: %s", e)
logger.warning("Warning: Error listing existing containers: %s", e)
return

for container in containers:
try:
container.stop()
except errors.APIError as e:
logging.warning("Warning: Error stopping container: %s", e)
logger.warning("Warning: Error stopping container: %s", e)

try:
container.remove()
except errors.APIError as e:
logging.warning("Warning: Error removing container: %s", e)
logger.warning("Warning: Error removing container: %s", e)

def _create_container(self) -> None:
port_bindings = {f"{self._internal_port}/tcp": None}
Expand Down Expand Up @@ -200,7 +202,7 @@ def _handle_image_pull_error(self, error) -> None:
f"Could not pull image and no local image available: {error}"
) from error

logging.warning("Warning: Could not pull image: %s. Using locally cached image.", error)
logger.warning("Warning: Could not pull image: %s. Using locally cached image.", error)

def stop(self) -> None:
self._stop_and_remove_container()
Expand All @@ -212,16 +214,16 @@ def _stop_and_remove_container(self) -> None:
try:
self._container.stop()
except errors.NotFound as e:
logging.warning("Warning: Container not found while stopping: %s", e)
logger.warning("Warning: Container not found while stopping: %s", e)
except errors.APIError as e:
logging.warning("Warning: API error while stopping container: %s", e)
logger.warning("Warning: API error while stopping container: %s", e)

try:
self._container.remove()
except errors.NotFound as e:
logging.warning("Warning: Container not found while removing: %s", e)
logger.warning("Warning: Container not found while removing: %s", e)
except errors.APIError as e:
logging.warning("Warning: API error while removing container: %s", e)
logger.warning("Warning: API error while removing container: %s", e)

self._container = None
self._mapped_port = None
Expand Down
22 changes: 11 additions & 11 deletions eventsourcingdb/event/event.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import json
from dataclasses import dataclass, field
from datetime import datetime
import json
from hashlib import sha256
from typing import Any, TypeVar

Expand Down Expand Up @@ -108,16 +108,16 @@ def parse(unknown_object: dict) -> "Event":
return event

def verify_hash(self) -> None:
metadata = "|".join([
self.spec_version,
self.event_id,
self.predecessor_hash,
self._time_from_server,
self.source,
self.subject,
self.type,
self.data_content_type,
])
metadata = (
f"{self.spec_version}|"
f"{self.event_id}|"
f"{self.predecessor_hash}|"
f"{self._time_from_server}|"
f"{self.source}|"
f"{self.subject}|"
f"{self.type}|"
f"{self.data_content_type}"
)

metadata_bytes = metadata.encode("utf-8")
data_bytes = json.dumps(
Expand Down
4 changes: 2 additions & 2 deletions eventsourcingdb/http_client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
from .response import Response

__all__ = [
"get_get_headers",
"get_post_headers",
"HttpClient",
"Response",
"get_get_headers",
"get_post_headers",
]
5 changes: 3 additions & 2 deletions eventsourcingdb/http_client/http_client.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from types import TracebackType
from typing import Self

import aiohttp
from aiohttp import ClientSession
Expand All @@ -18,13 +19,13 @@ def __init__(
self.__api_token = api_token
self.__session: ClientSession | None = None

async def __aenter__(self) -> 'HttpClient':
async def __aenter__(self) -> Self:
await self.__initialize()
return self

async def __aexit__(
self,
exc_type: BaseException | None = None,
exc_type: type[BaseException] | None = None,
exc_val: BaseException | None = None,
exc_tb: TracebackType | None = None,
) -> None:
Expand Down
5 changes: 3 additions & 2 deletions eventsourcingdb/http_client/response.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from collections.abc import Mapping
from http import HTTPStatus
from typing import Self

import aiohttp
from aiohttp import StreamReader
Expand All @@ -11,14 +12,14 @@ class Response:
def __init__(self, response: aiohttp.ClientResponse) -> None:
self.__response: aiohttp.ClientResponse = response

async def __aenter__(self) -> "Response":
async def __aenter__(self) -> Self:
return self

async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
if not self.__response.closed:
self.__response.close()

def __enter__(self) -> "Response":
def __enter__(self) -> Self:
return self

def __exit__(self, exc_type, exc_val, exc_tb) -> None:
Expand Down
5 changes: 1 addition & 4 deletions eventsourcingdb/is_valid_server_header.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,4 @@ def is_valid_server_header(response: Response) -> bool:
if not server_header:
return False

if not server_header.startswith('EventSourcingDB/'):
return False

return True
return server_header.startswith('EventSourcingDB/')
11 changes: 5 additions & 6 deletions eventsourcingdb/observe_events/observe_events_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,11 @@ def validate(self) -> None:
"ObserveEventsOptions are invalid: lower_bound must be a Bound object."
)

if self.from_latest_event is not None:
if self.lower_bound is not None:
raise ValidationError(
"ReadEventsOptions are invalid: "
"lowerBound and fromLatestEvent are mutually exclusive"
)
if self.from_latest_event is not None and self.lower_bound is not None:
raise ValidationError(
"ReadEventsOptions are invalid: "
"lowerBound and fromLatestEvent are mutually exclusive"
)

def to_json(self) -> dict[str, Any]:
result: dict[str, Any] = {
Expand Down
2 changes: 1 addition & 1 deletion eventsourcingdb/pandas.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import AsyncGenerator
from collections.abc import AsyncGenerator

import pandas as pd

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ dev = [
"pytest-timeout==2.4.0",
"pytest-asyncio==1.4.0",
"pytest-cov==7.1.0",
"ruff==0.15.22",
"ruff==0.16.0",
"bandit==1.9.4",
"pyright==1.1.411",
"twine==6.2.0",
Expand Down
8 changes: 5 additions & 3 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import ClassVar

import pytest_asyncio

from eventsourcingdb import EventCandidate
Expand All @@ -20,9 +22,9 @@ class TestData:
LOGGED_IN_SUBJECT = "/users/logged-in"
REGISTERED_TYPE = "io.thenativeweb.users.registered"
LOGGED_IN_TYPE = "io.thenativeweb.users.logged-in"
JANE_DATA = {"name": "jane"}
JOHN_DATA = {"name": "john"}
APFEL_FRED_DATA = {"name": "apfel fred"}
JANE_DATA: ClassVar[dict[str, str]] = {"name": "jane"}
JOHN_DATA: ClassVar[dict[str, str]] = {"name": "john"}
APFEL_FRED_DATA: ClassVar[dict[str, str]] = {"name": "apfel fred"}
TRACE_PARENT_1 = "00-10000000000000000000000000000000-1000000000000000-00"
TRACE_PARENT_2 = "00-20000000000000000000000000000000-2000000000000000-00"
TRACE_PARENT_3 = "00-30000000000000000000000000000000-3000000000000000-00"
Expand Down
5 changes: 3 additions & 2 deletions tests/event/test_verify_hash.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from hashlib import sha256

import pytest

from eventsourcingdb import EventCandidate
from eventsourcingdb.errors.validation_error import ValidationError
from hashlib import sha256

from ..conftest import TestData
from ..shared.database import Database
Expand Down Expand Up @@ -50,7 +51,7 @@ async def test_fails_if_the_event_hash_is_invalid(

written_event = written_events[0]

invalid_hash_data = "invalid data".encode("utf-8")
invalid_hash_data = b"invalid data"
invalid_hash = sha256(invalid_hash_data).hexdigest()
written_event.hash = invalid_hash

Expand Down
9 changes: 5 additions & 4 deletions tests/event/test_verify_signature.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import pytest
from hashlib import sha256

import pytest
from cryptography.hazmat.primitives.asymmetric import ed25519
from eventsourcingdb import EventCandidate, Container

from eventsourcingdb import Container, EventCandidate
from eventsourcingdb.errors.validation_error import ValidationError
from hashlib import sha256

from ..conftest import TestData

Expand Down Expand Up @@ -70,7 +71,7 @@ async def test_returns_error_if_hash_verification_fails(
written_event = written_events[0]
assert written_event.signature is not None

invalid_hash_data = "invalid hash".encode("utf-8")
invalid_hash_data = b"invalid hash"
invalid_hash = sha256(invalid_hash_data).hexdigest()
written_event.hash = invalid_hash

Expand Down
Loading