Skip to content

Commit

Permalink
Merge pull request #2 from MITLibraries/in-714-app-structure
Browse files Browse the repository at this point in the history
Set up initial CLI command and configuration
  • Loading branch information
hakbailey committed Feb 17, 2023
2 parents 42ee179 + 065c10c commit bb01a4f
Show file tree
Hide file tree
Showing 15 changed files with 911 additions and 82 deletions.
12 changes: 11 additions & 1 deletion .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,18 @@ updates:
schedule:
interval: "daily"

# Maintain dependencies for npm
# Maintain dependencies for application
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "daily"
reviewers:
- "MITLibraries/dataeng"

# Maintain dependencies for Docker
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
reviewers:
- "MITLibraries/dataeng"
3 changes: 1 addition & 2 deletions .github/pull-request-template.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,7 @@ Include links to Jira Software and/or Jira Service Management tickets here.

### Code Reviewer

- [ ] The commit message is clear and follows our guidelines
(not just this pull request message)
- [ ] The commit message is clear and follows our guidelines (not just this pull request message)
- [ ] There are appropriate tests covering any new functionality
- [ ] The documentation has been updated or is unnecessary
- [ ] The changes have been verified
Expand Down
2 changes: 1 addition & 1 deletion .python-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.10.3
3.11.2
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:3.10-slim as build
FROM python:3.11-slim as build
WORKDIR /app
COPY . .

Expand All @@ -9,4 +9,4 @@ RUN apt-get update && apt-get upgrade -y && apt-get install -y git
COPY Pipfile* /
RUN pipenv install

ENTRYPOINT ["pipenv", "run", "my_app"]
ENTRYPOINT ["pipenv", "run", "ccslips"]
6 changes: 3 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ update: install ## Update all Python dependencies
### Test commands ###

test: ## Run tests and print a coverage report
pipenv run coverage run --source=my_app -m pytest -vv
pipenv run coverage run --source=ccslips -m pytest -vv
pipenv run coverage report -m

coveralls: test
Expand All @@ -31,13 +31,13 @@ coveralls: test
lint: bandit black mypy pylama safety ## Run linting, code quality, and safety checks

bandit:
pipenv run bandit -r my_app
pipenv run bandit -r ccslips

black:
pipenv run black --check --diff .

mypy:
pipenv run mypy my_app
pipenv run mypy ccslips

pylama:
pipenv run pylama --options setup.cfg
Expand Down
5 changes: 3 additions & 2 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@ bandit = "*"
black = "*"
coverage = "*"
coveralls = "*"
freezegun = "*"
mypy = "*"
pylama = {extras = ["all"], version = "*"}
pytest = "*"

[requires]
python_version = "3.10"
python_version = "3.11"

[scripts]
my_app = "python -c \"from my_app.cli import main; main()\""
ccslips = "python -c \"from ccslips.cli import main; main()\""
762 changes: 762 additions & 0 deletions Pipfile.lock

Large diffs are not rendered by default.

31 changes: 5 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,38 +1,17 @@
# python-cli-template
# alma-creditcardslips

A template repository for creating Python CLI applications.

## App setup (delete this section and above after initial application setup)

1. Rename "my_app" to the desired app name across the repo. (May be helpful to do a project-wide find-and-replace).
2. Update Python version if needed.
3. Install all dependencies with `make install` to create initial Pipfile.lock with latest dependency versions.
4. Add initial app description to README and update initial required ENV variable documentation as needed.
5. Update license if needed (check app-specific dependencies for licensing terms).
6. Check Github repository settings:
- Confirm repo branch protection settings are correct (see [dev docs](https://mitlibraries.github.io/guides/basics/github.html) for details)
- Confirm that all of the following are enabled in the repo's code security and analysis settings:
- Dependabot alerts
- Dependabot security updates
- Secret scanning
7. Create a Sentry project for the app if needed (we want this for most apps):
- Send initial exceptions to Sentry project for dev, stage, and prod environments to create them.
- Create an alert for the prod environment only, with notifications sent to the appropriate team(s).
- If *not* using Sentry, delete Sentry configuration from config.py and test_config.py, and remove sentry_sdk from project dependencies.

# my_app

Description of the app
A CLI application to generate and email credit card slips for Alma invoices via the Alma API.

## Development

- To install with dev dependencies: `make install`
- To update dependencies: `make update`
- To run unit tests: `make test`
- To lint the repo: `make lint`
- To run the app: `pipenv run my_app --help`
- To run the app: `pipenv run ccslips --help`

## Required ENV
## ENV Variables

- `LOG_LEVEL` = Optional, set to a valid Python logging level (e.g. `DEBUG`, case-insensitive) if desired. Can also be passed as an option directly to the ccslips command. Defaults to `INFO` if not set or passed to the command.
- `SENTRY_DSN` = If set to a valid Sentry DSN, enables Sentry exception monitoring. This is not needed for local development.
- `WORKSPACE` = Set to `dev` for local development, this will be set to `stage` and `prod` in those environments by Terraform.
1 change: 1 addition & 0 deletions ccslips/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""ccslips package."""
71 changes: 71 additions & 0 deletions ccslips/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import logging
from datetime import datetime, timedelta
from time import perf_counter
from typing import Optional

import click

from ccslips.config import configure_logger, configure_sentry

logger = logging.getLogger(__name__)


@click.command()
@click.option(
"-s",
"--source-email",
required=True,
help="The email address sending the credit card slips.",
)
@click.option(
"-r",
"--recipient-email",
required=True,
multiple=True,
help="The email address receiving the credit card slips. Repeatable",
)
@click.option(
"-d",
"--date",
help=(
"Optional date of exports to process, in 'YYYY-MM-DD' format. Defaults to "
"yesterday's date if not provided."
),
)
@click.option(
"-l",
"--log-level",
envvar="LOG_LEVEL",
help="Case-insensitive Python log level to use, e.g. debug or warning. Defaults to "
"INFO if not provided or found in ENV.",
)
@click.pass_context
def main(
ctx: click.Context,
source_email: str,
recipient_email: list[str],
date: Optional[str],
log_level: Optional[str],
) -> None:
start_time = perf_counter()
log_level = log_level or "INFO"
root_logger = logging.getLogger()
logger.info(configure_logger(root_logger, log_level))
logger.info(configure_sentry())
logger.debug("Command called with options: %s", ctx.params)

logger.info("Starting credit card slips process")

# Do things here!
date = date or (datetime.today() - timedelta(days=1)).strftime("%Y-%m-%d")
click.echo(
f"\nFunctionality to be added here will process the credit card invoices from "
f"date {date} and send the resulting email from {source_email} to "
f"{recipient_email}\n"
)

elapsed_time = perf_counter() - start_time
logger.info(
"Finished! Total time to complete process: %s",
str(timedelta(seconds=elapsed_time)),
)
13 changes: 8 additions & 5 deletions my_app/config.py → ccslips/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,23 @@
import sentry_sdk


def configure_logger(logger: logging.Logger, verbose: bool) -> str:
if verbose:
def configure_logger(logger: logging.Logger, log_level_string: str) -> str:
if log_level_string.upper() not in logging.getLevelNamesMapping():
raise ValueError(f"'{log_level_string}' is not a valid Python logging level")
log_level = logging.getLevelName(log_level_string.upper())
if log_level < 20:
logging.basicConfig(
format="%(asctime)s %(levelname)s %(name)s.%(funcName)s() line %(lineno)d: "
"%(message)s"
)
logger.setLevel(logging.DEBUG)
logger.setLevel(log_level)
for handler in logging.root.handlers:
handler.addFilter(logging.Filter("my_app"))
handler.addFilter(logging.Filter("ccslips"))
else:
logging.basicConfig(
format="%(asctime)s %(levelname)s %(name)s.%(funcName)s(): %(message)s"
)
logger.setLevel(logging.INFO)
logger.setLevel(log_level)
return (
f"Logger '{logger.name}' configured with level="
f"{logging.getLevelName(logger.getEffectiveLevel())}"
Expand Down
1 change: 0 additions & 1 deletion my_app/__init__.py

This file was deleted.

28 changes: 0 additions & 28 deletions my_app/cli.py

This file was deleted.

35 changes: 29 additions & 6 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,40 @@
from my_app.cli import main
from freezegun import freeze_time

from ccslips.cli import main

def test_cli_no_options(caplog, runner):
result = runner.invoke(main)

@freeze_time("2023-01-02")
def test_cli_only_required_options(caplog, runner):
result = runner.invoke(
main, ["-s", "source@example.com", "-r", "recipient@example.com"]
)
assert result.exit_code == 0
assert "Logger 'root' configured with level=INFO" in caplog.text
assert "Running process" in caplog.text
assert "Starting credit card slips process" in caplog.text
assert "Total time to complete process" in caplog.text


def test_cli_all_options(caplog, runner):
result = runner.invoke(main, ["--verbose"])
result = runner.invoke(
main,
[
"--source-email",
"source@example.com",
"--recipient-email",
"recipient1@example.com",
"--recipient-email",
"recipient2@example.com",
"--date",
"2023-01-02",
"--log-level",
"debug",
],
)
assert result.exit_code == 0
assert "Logger 'root' configured with level=DEBUG" in caplog.text
assert "Running process" in caplog.text
assert (
"Command called with options: {'source_email': 'source@example.com', "
"'recipient_email': ('recipient1@example.com', 'recipient2@example.com'), "
"'date': '2023-01-02', 'log_level': 'debug'}" in caplog.text
)
assert "Total time to complete process" in caplog.text
19 changes: 14 additions & 5 deletions tests/test_config.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,27 @@
import logging

from my_app.config import configure_logger, configure_sentry
import pytest

from ccslips.config import configure_logger, configure_sentry

def test_configure_logger_not_verbose():

def test_configure_logger_with_invalid_level_raises_error():
logger = logging.getLogger(__name__)
with pytest.raises(ValueError) as error:
configure_logger(logger, log_level_string="oops")
assert "'oops' is not a valid Python logging level" in str(error)


def test_configure_logger_info_level_or_higher():
logger = logging.getLogger(__name__)
result = configure_logger(logger, verbose=False)
result = configure_logger(logger, log_level_string="info")
assert logger.getEffectiveLevel() == 20
assert result == "Logger 'tests.test_config' configured with level=INFO"


def test_configure_logger_verbose():
def test_configure_logger_debug_level_or_lower():
logger = logging.getLogger(__name__)
result = configure_logger(logger, verbose=True)
result = configure_logger(logger, log_level_string="DEBUG")
assert logger.getEffectiveLevel() == 10
assert result == "Logger 'tests.test_config' configured with level=DEBUG"

Expand Down

0 comments on commit bb01a4f

Please sign in to comment.