Skip to content

Commit

Permalink
Replace argparse with Typer
Browse files Browse the repository at this point in the history
  • Loading branch information
faboshka committed Aug 3, 2024
1 parent 952ccc6 commit 3431835
Show file tree
Hide file tree
Showing 12 changed files with 124 additions and 263 deletions.
59 changes: 20 additions & 39 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ And I know this could be written in one file - with a handful of code lines, but

To be able to install the project's dependencies, you need to have:

* [_Python_](https://www.python.org/) (>= _3.10.0_)
* [_Poetry_](https://python-poetry.org/) (>= _1.0.0_)
* [_Python_](https://www.python.org/) (>= _3.12.0_)
* [_Poetry_](https://python-poetry.org/) (>= _1.8.0_)
* [_Twilio_](https://www.twilio.com/) account with an active phone number

A few notes:
Expand Down Expand Up @@ -97,53 +97,34 @@ poetry install
To run the _Secret Santa_ package / module, all you need to do after setting it up is run:

```bash
python secret_santa
poetry run secret_santa
```

with the appropriate arguments.
with the appropriate commands / arguments.

To see all the supported command-line arguments, run `python secret_santa --help`.
To see all the supported command-line arguments, run `poetry run secret_santa --help`.

<details>
<summary>The command-line arguments supported</summary>

```console
❯ python secret_santa --help
____ _ ____ _
/ ___| ___ ___ _ __ ___| |_ / ___| __ _ _ __ | |_ __ _
\___ \ / _ \/ __| '__/ _ \ __| \___ \ / _` | '_ \| __/ _` |
___) | __/ (__| | | __/ |_ ___) | (_| | | | | || (_| |
|____/ \___|\___|_| \___|\__| |____/ \__,_|_| |_|\__\__,_|
usage: secret_santa [-h] [--env-path ENV_PATH] [--participants-path PARTICIPANTS_PATH] [--show-arrangement] [--dry-run] [-log {critical,error,warn,warning,info,debug}]
Welcome to the Secret Santa Organizer, in which each participant gets one other participant assigned, to whom he should bring a gift!
options:
-h, --help show this help message and exit
--env-path ENV_PATH path to the .env file containing the required secrets
If omitted - will try to load the "{project_root}/.env".
--> Note: No error will be raised in case --env-path is not provided and no "{project_root}/.env" exists.
--participants-path PARTICIPANTS_PATH
path to the "Secret Santa" participants JSON
If omitted - will try to load "{project_root}/participants.json".
--show-arrangement show the final arrangement (participant -> receiver)
--> Note: The --show-arrangement is shown as logged INFO, which means setting the logger any higher will not show the arrangements as expected.
--> Personal Request: Please keep it fun and use this only for development-testing purposes / if you're a non-participating admin.
--dry-run run the program without actually sending the message
-log {critical,error,warn,warning,info,debug}, --logging-level {critical,error,warn,warning,info,debug}
set the main logging level of the program loggers (Defaults to "info")
Merry Christmas! and have lots of fun :)
```
</details>

**_Note:_** The program could alternatively be run as a module:
❯ poetry run secret_santa run --help
Usage: secret_santa run [OPTIONS]
run the secret santa game
╭─ Options ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ * --participants-path PATH path to the 'Secret Santa' participants JSON [default: None] [required] │
│ --env-path PATH path to the 'Secret Santa' environment [default: None] │
│ --show-arrangement --hide-arrangement show the final arrangement (participant -> receiver) [default: hide-arrangement] │
│ --logging-level [critical|error|warning|info|debug] logging level [default: info] │
│ --dry-run --no-dry-run run the program without actually sending the message [default: no-dry-run] │
│ --help Show this message and exit. │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯

```bash
python -m secret_santa
```
</details>

## Future Plans

Expand Down
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ python = "^3.12"
python-dotenv = "^1.0.1"
twilio = "^9.2.3"
pyfiglet = "^1.0.2"
typer-slim = { version = "^0.12.3", extras = ["standard"] }

[tool.poetry.group.dev.dependencies]
taskipy = "^1.13.0"
Expand All @@ -29,6 +30,9 @@ pytest-lazy-fixtures = "^1.1.1"
requires = ["poetry-core>=1.8.0"]
build-backend = "poetry.core.masonry.api"

[tool.poetry.scripts]
secret_santa = "secret_santa.client.app:secret_santa_app"

[tool.ruff]
line-length = 120

Expand Down
8 changes: 0 additions & 8 deletions secret_santa/__main__.py

This file was deleted.

1 change: 1 addition & 0 deletions secret_santa/client/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Secret Santa client app package."""
57 changes: 57 additions & 0 deletions secret_santa/client/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""The secret santa app."""

import time
from pathlib import Path
from typing import Annotated, Optional

import pyfiglet
from typer import Option, Typer

from secret_santa.secret_santa_module import SecretSanta, load_env
from secret_santa.util import logging
from secret_santa.util.logging import LoggingLevel

secret_santa_app = Typer(
short_help="Secret Santa client app.",
help="Welcome to the Secret Santa Organizer, in which each participant gets one other participant assigned, "
"to whom he should bring a gift!",
no_args_is_help=True,
rich_markup_mode="rich",
epilog="Merry Christmas! and have lots of fun :)",
add_completion=False,
)


@secret_santa_app.command(help="run the secret santa game", no_args_is_help=True)
def run(
participants_path: Annotated[Path, Option(..., help="path to the 'Secret Santa' participants JSON")],
env_path: Annotated[Optional[Path], Option(..., help="path to the 'Secret Santa' environment")] = None,
show_arrangement: Annotated[
bool,
Option(
..., "--show-arrangement/--hide-arrangement", help="show the final arrangement (participant -> receiver)"
),
] = False,
logging_level: Annotated[LoggingLevel, Option(..., case_sensitive=False, help="logging level")] = LoggingLevel.info,
dry_run: Annotated[bool, Option(..., help="run the program without actually sending the message")] = False,
) -> int:
"""Run the secret santa game."""
secret_santa_figlet = pyfiglet.figlet_format("Secret Santa")
print(secret_santa_figlet) # noqa: T201
time.sleep(0.5)
logging.get_logger(add_common_handler=False).setLevel(str(logging_level).upper())
load_env(env_path)
return SecretSanta(
participants_json_path=participants_path,
show_arrangement=show_arrangement,
dry_run=dry_run,
).run()


@secret_santa_app.command(help="run the secret santa game", no_args_is_help=True, hidden=True)
def validate() -> None:
"""Validate the secret santa game's participants."""


if __name__ == "__main__":
secret_santa_app()
2 changes: 2 additions & 0 deletions secret_santa/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@
TWILIO_NUMBER = "TWILIO_NUMBER"

MINIMUM_NUMBER_OF_PARTICIPANTS = 3

ENCODING = "utf-8"
34 changes: 1 addition & 33 deletions secret_santa/secret_santa_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,18 @@

import copy
import json
import logging
import os
import random
import time
from os import PathLike
from pathlib import Path
from typing import Optional

import pyfiglet
from dotenv import load_dotenv

from secret_santa.const import MINIMUM_NUMBER_OF_PARTICIPANTS, TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_NUMBER
from secret_santa.model.participant import Participant
from secret_santa.twilio_messaging_service import TwilioMessagingService
from secret_santa.util import arg_parser, file, misc, path
from secret_santa.util import file, misc, path
from secret_santa.util import logging as logging_util

# Set up the main logger
Expand Down Expand Up @@ -219,32 +216,3 @@ def load_env(dotenv_path: Optional[PathLike] = None, override_system: bool = Fal
environment_err = f"One or more of the environment variables needed ({required_env_vars}) has not been passed."
raise SystemExit(environment_err)
logger.info("Environment loaded successfully")


def main() -> int:
"""The module's main function.
Returns:
Zero in case the ``SecretSanta`` class is initialized and run as should be, non-Zero code otherwise.
"""
secret_santa_figlet = pyfiglet.figlet_format("Secret Santa")
print(secret_santa_figlet) # noqa: T201
time.sleep(0.5)
# Parse the provided arguments
parser = arg_parser.get_secret_santa_argument_parser()
args = parser.parse_args()
# Get the root logger and update its level to set the main logging level
logging.getLogger().setLevel(logging_util.logging_levels.get(args.logging_level, "info"))
# Load the environment
load_env(args.env_path)
# Initialize the Secret Santa module and run it to send a message to the participants
return SecretSanta(
participants_json_path=args.participants_path,
show_arrangement=args.show_arrangement,
dry_run=args.dry_run,
).run()


if __name__ == "__main__":
main()
90 changes: 0 additions & 90 deletions secret_santa/util/arg_parser.py

This file was deleted.

6 changes: 4 additions & 2 deletions secret_santa/util/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
from os import PathLike
from pathlib import Path

from secret_santa.const import ENCODING


def create_file(full_file_name: PathLike, content: str) -> None:
"""Create a new file containing the ``content`` at the path specified ``full_file_name``.
Expand All @@ -12,7 +14,7 @@ def create_file(full_file_name: PathLike, content: str) -> None:
content: The content to write to the file.
"""
Path(full_file_name).write_text(content, encoding="utf8")
Path(full_file_name).write_text(content, encoding=ENCODING)


def read_file(file_name: PathLike) -> str:
Expand All @@ -26,4 +28,4 @@ def read_file(file_name: PathLike) -> str:
as a list of strings read line by line in case ``read_line_by_line`` is True.
"""
return Path(file_name).read_text(encoding="utf8")
return Path(file_name).read_text(encoding=ENCODING)
30 changes: 23 additions & 7 deletions secret_santa/util/logging.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,34 @@
"""logging utilities."""

import logging
from enum import StrEnum, auto
from typing import Optional

logging_levels: dict[str, int] = {
"critical": logging.CRITICAL,
"error": logging.ERROR,
"warn": logging.WARNING,
"warning": logging.WARNING,
"info": logging.INFO,
"debug": logging.DEBUG,

class LoggingLevel(StrEnum):
"""App supported logging levels."""

critical = auto()
error = auto()
warning = auto()
info = auto()
debug = auto()


LOGGING_LEVEL_TO_NUMBER: dict[LoggingLevel, int] = {
LoggingLevel.critical: logging.CRITICAL,
LoggingLevel.error: logging.ERROR,
LoggingLevel.warning: logging.WARNING,
LoggingLevel.info: logging.INFO,
LoggingLevel.debug: logging.DEBUG,
}


def logging_level_to_number(log_level: LoggingLevel) -> int:
"""Logging level to number."""
return logging.getLevelNamesMapping()[str(log_level).upper()]


class CustomLogFormatter(logging.Formatter):
"""A super class of ``logging.Formatter`` solemnly for custom log message formatting."""

Expand Down
Loading

0 comments on commit 3431835

Please sign in to comment.