Skip to content

Commit 034d174

Browse files
committed
update
1 parent 38ab9f5 commit 034d174

35 files changed

+967
-1
lines changed

.coverage

52 KB
Binary file not shown.

.coveragerc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[paths]
2+
source =
3+
src
4+
.tox/*/lib/python*/site-packages

.github/workflows/main.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: tox-pytest-Okken-2022
2+
3+
# Run tests every time either code is pushed to the repository or a pull request is created
4+
on: [push, pull_request]
5+
6+
jobs:
7+
build:
8+
9+
# Specify which operating system to run the tests on
10+
runs-on: ubuntu-latest
11+
strategy:
12+
matrix:
13+
# Specify which Python versions to run
14+
python: ["3.10", "3.11", "3.12"]
15+
16+
steps:
17+
# Check out the repository by using the `actions/checkout@v2` GitHub tool
18+
# so the rest of the workflow can access it
19+
- uses: actions/checkout@v2
20+
- name: Setup Python
21+
# Get Python configured and installed in a build environment by using the `setup-python@v2` GitHub tool
22+
uses: actions/setup-python@v2
23+
with:
24+
# Create an environment for each of the Python versions listed in matrix: python
25+
python-version: ${{ matrix.python }}
26+
- name: Install Tox and any other packages
27+
# Install tox
28+
run: pip install tox
29+
- name: Run Tox
30+
# Run tox selecting the correct version of Python specified in `tox.ini`
31+
run: tox -e py -c tox_github_actions.ini

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2018, Brian Okken
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
# GitHub-Actions-tox-pytest
1+
...

pyproject.toml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
[build-system]
2+
requires = ["flit_core >=3.2,<4"]
3+
build-backend = "flit_core.buildapi"
4+
5+
[project]
6+
name = "cards"
7+
authors = [{name = "Brian Okken", email = "brian+pypi@pythontest.com"}]
8+
classifiers = [
9+
"License :: OSI Approved :: MIT License",
10+
"Programming Language :: Python",
11+
"Programming Language :: Python :: 3",
12+
"Programming Language :: Python :: 3.7",
13+
"Programming Language :: Python :: 3.8",
14+
"Programming Language :: Python :: 3.9",
15+
"Programming Language :: Python :: 3.10",
16+
]
17+
requires-python=">=3.7"
18+
dynamic = ["version", "description"]
19+
dependencies = [
20+
"tinydb==4.5.1",
21+
"typer==0.3.2",
22+
"rich==10.7.0"
23+
]
24+
25+
[project.optional-dependencies]
26+
test = [
27+
"pytest",
28+
"faker",
29+
"tox",
30+
"coverage",
31+
"pytest-cov",
32+
]
33+
34+
[project.urls]
35+
Home = "https://github.com/okken/cards"
36+
37+
[project.scripts]
38+
cards = "cards:app"
39+

pytest.ini

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
;---
2+
; Excerpted from "Python Testing with pytest, Second Edition",
3+
; published by The Pragmatic Bookshelf.
4+
; Copyrights apply to this code. It may not be used to create training material,
5+
; courses, books, articles, and the like. Contact us if you are in doubt.
6+
; We make no guarantees that this code is fit for any purpose.
7+
; Visit https://pragprog.com/titles/bopytest2 for more book information.
8+
;---
9+
[pytest]
10+
addopts =
11+
--strict-markers
12+
--strict-config
13+
-ra
14+
15+
markers =
16+
num_cards: number of cards to prefill for cards_db fixture
17+
18+
testpaths = tests

src/cards/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""Top-level package for cards."""
2+
3+
__version__ = "1.0.0"
4+
5+
from .api import * # noqa
6+
from .cli import app # noqa

src/cards/api.py

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
"""
2+
API for the cards project
3+
"""
4+
from dataclasses import asdict
5+
from dataclasses import dataclass
6+
from dataclasses import field
7+
from typing import List
8+
import cards
9+
10+
from .db import DB
11+
12+
__all__ = [
13+
"Card",
14+
"CardsDB",
15+
"CardsException",
16+
"MissingSummary",
17+
"InvalidCardId",
18+
]
19+
20+
21+
@dataclass
22+
class Card:
23+
summary: str = None
24+
owner: str = None
25+
state: str = "todo"
26+
id: int = field(default=None, compare=False)
27+
28+
@classmethod
29+
def from_dict(cls, d):
30+
return Card(**d)
31+
32+
def to_dict(self):
33+
return asdict(self)
34+
35+
36+
class CardsException(Exception):
37+
pass
38+
39+
40+
class MissingSummary(CardsException):
41+
pass
42+
43+
44+
class InvalidCardId(CardsException):
45+
pass
46+
47+
48+
class CardsDB:
49+
def __init__(self, db_path):
50+
self._db_path = db_path
51+
self._db = DB(db_path, ".cards_db")
52+
53+
def add_card(self, card: Card) -> int:
54+
"""Add a card, return the id of card."""
55+
if not card.summary:
56+
raise MissingSummary
57+
if card.owner is None:
58+
card.owner = ""
59+
id = self._db.create(card.to_dict())
60+
self._db.update(id, {"id": id})
61+
return id
62+
63+
def get_card(self, card_id: int) -> Card:
64+
"""Return a card with a matching id."""
65+
db_item = self._db.read(card_id)
66+
if db_item is not None:
67+
return Card.from_dict(db_item)
68+
else:
69+
raise InvalidCardId(card_id)
70+
71+
def list_cards(self, owner=None, state=None):
72+
"""Return a list of cards."""
73+
all = self._db.read_all()
74+
if (owner is not None) and (state is not None):
75+
return [
76+
Card.from_dict(t)
77+
for t in all
78+
if (t["owner"] == owner and t["state"] == state)
79+
]
80+
elif owner is not None:
81+
return [
82+
Card.from_dict(t) for t in all if t["owner"] == owner
83+
]
84+
elif state is not None:
85+
return [
86+
Card.from_dict(t) for t in all if t["state"] == state
87+
]
88+
else:
89+
return [Card.from_dict(t) for t in all]
90+
91+
def count(self) -> int:
92+
"""Return the number of cards in db."""
93+
return self._db.count()
94+
95+
def update_card(self, card_id: int, card_mods: Card) -> None:
96+
"""Update a card with modifications."""
97+
try:
98+
self._db.update(card_id, card_mods.to_dict())
99+
except KeyError as exc:
100+
raise InvalidCardId(card_id) from exc
101+
102+
def start(self, card_id: int):
103+
"""Set a card state to 'in prog'."""
104+
self.update_card(card_id, Card(state="in prog"))
105+
106+
def finish(self, card_id: int):
107+
"""Set a card state to 'done'."""
108+
self.update_card(card_id, Card(state="done"))
109+
110+
def delete_card(self, card_id: int) -> None:
111+
"""Remove a card from db with given card_id."""
112+
try:
113+
self._db.delete(card_id)
114+
except KeyError as exc:
115+
raise InvalidCardId(card_id) from exc
116+
117+
def delete_all(self) -> None:
118+
"""Remove all cards from db."""
119+
self._db.delete_all()
120+
121+
def close(self):
122+
self._db.close()
123+
124+
def path(self):
125+
return self._db_path

src/cards/cli.py

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
"""Command Line Interface (CLI) for cards project."""
2+
import os
3+
from io import StringIO
4+
import pathlib
5+
import rich
6+
from rich.table import Table
7+
from contextlib import contextmanager
8+
from typing import List
9+
10+
import cards
11+
12+
import typer
13+
14+
app = typer.Typer(add_completion=False)
15+
16+
@app.command()
17+
def version():
18+
"""Return version of cards application"""
19+
print(cards.__version__)
20+
21+
22+
@app.command()
23+
def add(
24+
summary: List[str], owner: str = typer.Option(None, "-o", "--owner")
25+
):
26+
"""Add a card to db."""
27+
summary = " ".join(summary) if summary else None
28+
with cards_db() as db:
29+
db.add_card(cards.Card(summary, owner, state="todo"))
30+
31+
32+
@app.command()
33+
def delete(card_id: int):
34+
"""Remove card in db with given id."""
35+
with cards_db() as db:
36+
try:
37+
db.delete_card(card_id)
38+
except cards.InvalidCardId:
39+
print(f"Error: Invalid card id {card_id}")
40+
41+
42+
@app.command("list")
43+
def list_cards(
44+
owner: str = typer.Option(None, "-o", "--owner"),
45+
state: str = typer.Option(None, "-s", "--state"),
46+
):
47+
"""
48+
List cards in db.
49+
"""
50+
with cards_db() as db:
51+
the_cards = db.list_cards(owner=owner, state=state)
52+
table = Table(box=rich.box.SIMPLE)
53+
table.add_column("ID")
54+
table.add_column("state")
55+
table.add_column("owner")
56+
table.add_column("summary")
57+
for t in the_cards:
58+
owner = "" if t.owner is None else t.owner
59+
table.add_row(str(t.id), t.state, owner, t.summary)
60+
out = StringIO()
61+
rich.print(table, file=out)
62+
print(out.getvalue())
63+
64+
65+
@app.command()
66+
def update(
67+
card_id: int,
68+
owner: str = typer.Option(None, "-o", "--owner"),
69+
summary: List[str] = typer.Option(None, "-s", "--summary"),
70+
):
71+
"""Modify a card in db with given id with new info."""
72+
summary = " ".join(summary) if summary else None
73+
with cards_db() as db:
74+
try:
75+
db.update_card(
76+
card_id, cards.Card(summary, owner, state=None)
77+
)
78+
except cards.InvalidCardId:
79+
print(f"Error: Invalid card id {card_id}")
80+
81+
82+
@app.command()
83+
def start(card_id: int):
84+
"""Set a card state to 'in prog'."""
85+
with cards_db() as db:
86+
try:
87+
db.start(card_id)
88+
except cards.InvalidCardId:
89+
print(f"Error: Invalid card id {card_id}")
90+
91+
92+
@app.command()
93+
def finish(card_id: int):
94+
"""Set a card state to 'done'."""
95+
with cards_db() as db:
96+
try:
97+
db.finish(card_id)
98+
except cards.InvalidCardId:
99+
print(f"Error: Invalid card id {card_id}")
100+
101+
102+
@app.command()
103+
def config():
104+
"""List the path to the Cards db."""
105+
with cards_db() as db:
106+
print(db.path())
107+
108+
109+
@app.command()
110+
def count():
111+
"""Return number of cards in db."""
112+
with cards_db() as db:
113+
print(db.count())
114+
115+
116+
@app.callback(invoke_without_command=True)
117+
def main(ctx: typer.Context):
118+
"""
119+
Cards is a small command line task tracking application.
120+
"""
121+
if ctx.invoked_subcommand is None:
122+
list_cards(owner=None, state=None)
123+
124+
125+
def get_path():
126+
db_path_env = os.getenv("CARDS_DB_DIR", "")
127+
if db_path_env:
128+
db_path = pathlib.Path(db_path_env)
129+
else:
130+
db_path = pathlib.Path.home() / "cards_db"
131+
return db_path
132+
133+
134+
@contextmanager
135+
def cards_db():
136+
db_path = get_path()
137+
db = cards.CardsDB(db_path)
138+
yield db
139+
db.close()

0 commit comments

Comments
 (0)