Skip to content

Commit

Permalink
Added docstrings
Browse files Browse the repository at this point in the history
  • Loading branch information
pachovit committed Jul 29, 2024
1 parent 8763879 commit c180525
Show file tree
Hide file tree
Showing 12 changed files with 295 additions and 14 deletions.
7 changes: 7 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ For the main features of the application, we use BDD. The Gherkin features can b

The reason behind choosing `pytest-bdd` versus `behave`, was to have a centralized way of running tests and evaluating coverage, including the functional tests.

### Coding Style

We favor the use of type hints and meaningful naming, over excesively descriptive docstrings. The used
docstring style focus on description of the functions and variables, additional context, and examples.

We use [ruff](https://github.com/astral-sh/ruff) in order to lint and format code.

### CI/CD

We try to be as close as possible to trunk-based-development and Continuous Delivery. For that, the main branch has a pipeline with the following steps:
Expand Down
13 changes: 13 additions & 0 deletions echopages/application/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@ def add_content(
uow: repositories.UnitOfWork,
content_data: Dict[str, str],
) -> int:
"""
Adds a new content to the content repository.
Args:
uow (repositories.UnitOfWork): The unit of work instance.
content_data (Dict[str, str]): A dictionary containing the content data.
Returns:
int: The ID of the newly added content.
Raises:
None.
"""
with uow:
content = model.Content(id=None, **content_data)
content_id = uow.content_repo.add(content)
Expand Down
12 changes: 12 additions & 0 deletions echopages/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,17 @@


def get_unit_of_work() -> UnitOfWork:
"""
Get a unit of work for the application based on the configuration.
"""
config = echopages.config.get_config()
return FileUnitOfWork(config.db_uri)


def get_digest_delivery_system() -> DigestDeliverySystem:
"""
Get the digest delivery system based on the configuration.
"""
config = echopages.config.get_config()
if config.delivery_system == "DiskDigestDeliverySystem":
return delivery_system.DiskDigestDeliverySystem(
Expand All @@ -23,8 +29,14 @@ def get_digest_delivery_system() -> DigestDeliverySystem:


def get_sampler() -> samplers.SimpleContentSampler:
"""
Get the content sampler based on the configuration.
"""
return samplers.SimpleContentSampler()


def get_digest_formatter() -> DigestFormatter:
"""
Get the digest formatter based on the configuration.
"""
return delivery_system.HTMLDigestFormatter()
21 changes: 21 additions & 0 deletions echopages/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,33 @@


class Config(BaseSettings):
# API settings
api_host: str = "0.0.0.0"
api_port: int = 8000

# Database URI
db_uri: str = "data/echopages.db"

# Delivery system to use
delivery_system: str = "PostmarkDigestDeliverySystem"

# Directory for digests if using the DiskDigestDeliverySystem
disk_delivery_system_directory: str = "data/digests"

# Recipient email address
recipient_email: str = "recipient@echopages.com"

# Number of content units per digest
number_of_units_per_digest: int = 1

# Time of daily digest in the format HH:MM
daily_time_of_digest: str = "07:00"

@field_validator("daily_time_of_digest")
def validate_time(cls, v: str) -> str:
"""
Validate the daily time of digest format.
"""
if v is not None:
try:
datetime.strptime(v, "%H:%M")
Expand All @@ -26,6 +44,9 @@ def validate_time(cls, v: str) -> str:


def get_config() -> Config:
"""
Returns a singleton configuration object.
"""
global config
if config is None:
config = Config()
Expand Down
101 changes: 101 additions & 0 deletions echopages/domain/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,19 @@
from typing import Any, Callable, Dict, List, NewType, Optional


@dataclass
class Content:
"""
A Content unit.
Attributes:
id: The ID of the content item.
source: The source of the content item. E.g. book name, or article name.
author: The author of the content item.
location: The location of the content item. E.g. chapter 1, section 1.1, or page 75.
text: The actual content.
"""

def __init__(
self,
id: Optional[int] = None,
Expand All @@ -26,11 +38,24 @@ def __init__(

@dataclass
class DigestRepr:
"""
A representation of a Digest.
"""

title: DigestTitle
contents_str: DigestContentStr


class Digest:
"""
A Digest is a collection of content items that is sent out to the user.
Attributes:
id: The ID of the Digest.
content_ids: The IDs of the Content items included in the Digest.
sent_at: The time the Digest was sent.
"""

def __init__(
self,
id: Optional[int] = None,
Expand All @@ -42,14 +67,23 @@ def __init__(
self.sent_at = sent_at

def mark_as_sent(self) -> None:
"""
Mark the Digest as sent, setting the sent_at attribute to the current time.
"""
self.sent_at = datetime.now()

def to_dict(self) -> Dict[str, Any]:
"""
Convert the Digest to a dictionary representation.
"""
sent_at = self.sent_at.isoformat() if self.sent_at else None
return {"id": self.id, "content_ids": self.content_ids, "sent_at": sent_at}

@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "Digest":
"""
Create a Digest object from a dictionary representation.
"""
return cls(
id=data["id"],
content_ids=data["content_ids"],
Expand All @@ -60,26 +94,72 @@ def from_dict(cls, data: Dict[str, Any]) -> "Digest":


class ContentSampler(abc.ABC):
"""
Abstract base class for Content Samplers. Content Samplers are used to
choose which content units to include in a Digest.
"""

@abc.abstractmethod
def sample(
self, digests: List[Digest], contents: List[Content], number_of_units: int
) -> List[Content]:
"""
Sample a given number of Contents, from the previous Digests and the available Contents.
Args:
digests: The digests to sample from.
contents: The contents to sample from.
number_of_units: The number of units to sample.
Returns:
The sampled contents.
"""
raise NotImplementedError


class DigestFormatter(abc.ABC):
"""
Abstract base class for digest formatters. Digest formatters are used to
format some contents into a digest representation.
"""

@abc.abstractmethod
def format(self, contents: List[Content]) -> DigestRepr:
"""
Format the given contents into a Digest Representation.
Args:
contents: The contents to format.
Returns:
The formatted digest representation.
"""
raise NotImplementedError


class DigestDeliverySystem(abc.ABC):
"""
Abstract base class for digest delivery systems. Digest delivery systems
are used to deliver digests to the user.
"""

@abc.abstractmethod
def deliver_digest(self, digest_repr: DigestRepr) -> None:
"""
Deliver the given digest representation.
Args:
digest_repr: The digest representation to deliver.
"""
raise NotImplementedError


class Scheduler(abc.ABC):
"""
Abstract base class for schedulers. Schedulers are used to schedule
the execution of any function at a given time of day.
"""

@abc.abstractmethod
def __init__(
self,
Expand All @@ -88,16 +168,37 @@ def __init__(
time_zone: str = "Europe/Berlin",
sleep_interval: float = 1.0,
) -> None:
"""
Initializes a Scheduler object.
Args:
function: The function to be scheduled.
time_of_day: The time of day to schedule the function. Defaults to None.
time_zone: The time zone to use. Defaults to "Europe/Berlin".
sleep_interval : The sleep interval to wait to see if the time to trigger has arrived. Defaults to 1.0.
"""
raise NotImplementedError

@abc.abstractmethod
def configure_schedule(self, time_of_day: str) -> None:
"""
Configure the schedule for the given time of day.
Args:
time_of_day: The time of day to schedule the function.
"""
raise NotImplementedError

@abc.abstractmethod
def start(self) -> None:
"""
Start the scheduler.
"""
raise NotImplementedError

@abc.abstractmethod
def stop(self) -> None:
"""
Stop the scheduler.
"""
raise NotImplementedError
50 changes: 50 additions & 0 deletions echopages/domain/repositories.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,22 +39,72 @@ def update(self, digest: model.Digest) -> None:


class UnitOfWork(abc.ABC):
"""
Abstract base class for a unit of work.
A unit of work is a transactional object that represents a single logical
unit of work. It is used to encapsulate the business logic that needs to
be executed in a single transaction.
The unit of work is responsible for managing the state of the repositories
and ensuring that the changes are committed atomically.
Example usage:
```
with uow:
# Do some business logic
digest = uow.digest_repo.get_by_id(1)
digest.content_ids.append(2)
```
Attributes:
content_repo: The content repository.
digest_repo: The digest repository.
"""

content_repo: ContentRepository
digest_repo: DigestRepository

def __enter__(self) -> UnitOfWork:
"""
Enter the unit of work.
"""
return self

def __exit__(self, *args) -> None: # type: ignore
"""
Exit the unit of work.
If an exception is raised during the execution of the unit of work,
the changes are rolled back.
"""
self.rollback()

def commit(self) -> None:
"""
Commit the changes made in the unit of work.
This method should be called after all the changes have been made to
the repositories. It will commit the changes atomically.
"""
self._commit()

@abc.abstractmethod
def _commit(self) -> None:
"""
Commit the changes made in the unit of work.
This method should be implemented by subclasses to provide the
functionality to commit the changes made in the unit of work.
"""
raise NotImplementedError

@abc.abstractmethod
def rollback(self) -> None:
"""
Rollback the changes made in the unit of work.
This method should be implemented by subclasses to provide the
functionality to rollback the changes made in the unit of work.
"""
raise NotImplementedError
Loading

0 comments on commit c180525

Please sign in to comment.