MIL-001: Project foundation #38
+77
@@ -0,0 +1,77 @@
|
|||||||
|
# Local secrets: tokens are for personal use and are never part of the project
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
|
||||||
|
# Learner's own progress (created by the program on the first "known" answer)
|
||||||
|
data/words_to_learn.csv
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
|
||||||
|
# Generated documentation (doxygen Doxyfile)
|
||||||
|
docs/doxygen/
|
||||||
|
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[codz]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py.cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
|
||||||
|
# Type checkers and linters
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
.pyre/
|
||||||
|
.pytype/
|
||||||
|
.ruff_cache/
|
||||||
|
.import_linter_cache/
|
||||||
|
|
||||||
|
# Jupyter, IDEs and operating systems
|
||||||
|
.ipynb_checkpoints
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# Doxygen configuration for the flash card program.
|
||||||
|
# Build: doxygen Doxyfile (output: docs/doxygen/html/index.html)
|
||||||
|
# Only the settings that differ from the Doxygen defaults are listed.
|
||||||
|
|
||||||
|
PROJECT_NAME = "Flash Card"
|
||||||
|
PROJECT_BRIEF = "Malay to Danish flash cards (Udemy 100 Days of Code, day 31)"
|
||||||
|
PROJECT_NUMBER = 0.1.0
|
||||||
|
OUTPUT_DIRECTORY = docs/doxygen
|
||||||
|
CREATE_SUBDIRS = NO
|
||||||
|
|
||||||
|
INPUT = src
|
||||||
|
FILE_PATTERNS = *.py
|
||||||
|
RECURSIVE = YES
|
||||||
|
EXCLUDE_PATTERNS = */__pycache__/* */.venv/*
|
||||||
|
|
||||||
|
# Python
|
||||||
|
OPTIMIZE_OUTPUT_JAVA = YES
|
||||||
|
PYTHON_DOCSTRING = NO
|
||||||
|
EXTRACT_ALL = NO
|
||||||
|
EXTRACT_PRIVATE = NO
|
||||||
|
EXTRACT_STATIC = YES
|
||||||
|
JAVADOC_AUTOBRIEF = YES
|
||||||
|
AUTOLINK_SUPPORT = YES
|
||||||
|
MARKDOWN_SUPPORT = YES
|
||||||
|
|
||||||
|
# Diagrams need Graphviz; leave them off so the build works everywhere.
|
||||||
|
HAVE_DOT = NO
|
||||||
|
|
||||||
|
# Output
|
||||||
|
GENERATE_HTML = YES
|
||||||
|
GENERATE_LATEX = NO
|
||||||
|
SOURCE_BROWSER = YES
|
||||||
|
QUIET = YES
|
||||||
|
WARN_IF_UNDOCUMENTED = YES
|
||||||
|
WARN_IF_DOC_ERROR = YES
|
||||||
|
WARN_NO_PARAMDOC = YES
|
||||||
@@ -1,2 +1,132 @@
|
|||||||
# 031-flash-card
|
# 🚀 Flash Card
|
||||||
|
|
||||||
|
A flash card program for learning the most frequent Malay words with their Danish translations. It is the solution of day 31 of Udemy's *100 Days of Code: The Complete Python Pro Bootcamp*, written for course participants who want to compare solutions and for visitors who browse the repository for ideas.
|
||||||
|
|
||||||
|
## 📚 Table of Contents
|
||||||
|
|
||||||
|
- [Overview](#-overview)
|
||||||
|
- [Requirements](#-requirements)
|
||||||
|
- [Setup](#-setup)
|
||||||
|
- [Run](#-run)
|
||||||
|
- [Tests](#-tests)
|
||||||
|
- [License](#-license)
|
||||||
|
- [Links](#-links)
|
||||||
|
- [Appendix: Project documents](#-appendix-project-documents)
|
||||||
|
|
||||||
|
## 🧭 Overview
|
||||||
|
|
||||||
|
The program shows a Malay word on a card. After three seconds the card turns over and shows the Danish translation. You press the green tick when you know the word and the red cross when you do not. A known word is removed from the words to learn, and the words still to learn are saved, so the next start continues where you stopped.
|
||||||
|
|
||||||
|
| Part | What it is for |
|
||||||
|
| --- | --- |
|
||||||
|
| `src/flash_card/` | The program, in four layers: `domain`, `application`, `infrastructure`, `presentation` |
|
||||||
|
| `tests/` | `pytest` tests: `unit`, `integration`, `e2e` |
|
||||||
|
| `data/` | The word list `ms-da.csv` and, after the first known word, your own `words_to_learn.csv` |
|
||||||
|
| `docs/` | Project documents (business case, plan, design, review records) |
|
||||||
|
| `Doxyfile` | Doxygen configuration for the source code documentation |
|
||||||
|
|
||||||
|
The core program uses only the Python standard library. Spoken Malay words are an optional extra.
|
||||||
|
|
||||||
|
## 📋 Requirements
|
||||||
|
|
||||||
|
| Need | Why |
|
||||||
|
| --- | --- |
|
||||||
|
| Python 3.13 or later, with Tkinter | The program and its window (Tkinter is part of the standard Python installers for Windows and macOS; on Debian or Ubuntu install `python3-tk`) |
|
||||||
|
| `git` | To clone the repository |
|
||||||
|
| Doxygen (optional) | To build the source code documentation |
|
||||||
|
| `gTTS` and `playsound==1.2.2` (optional, extra `speech`) | To hear the Malay word; needs a network connection |
|
||||||
|
|
||||||
|
## 🛠️ Setup
|
||||||
|
|
||||||
|
Create a virtual environment in `.venv`, activate it, upgrade pip and install the project. Run the commands in the repository folder.
|
||||||
|
|
||||||
|
Windows (PowerShell):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m venv .venv
|
||||||
|
.\.venv\Scripts\Activate.ps1
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
python -m pip install -e ".[dev]"
|
||||||
|
```
|
||||||
|
|
||||||
|
Linux and macOS:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
python -m pip install -e ".[dev]"
|
||||||
|
```
|
||||||
|
|
||||||
|
- Use `python -m pip install -e .` if you only want to run the program; the `dev` extra adds `pytest`, `ruff`, `mypy` and `import-linter`.
|
||||||
|
- Add the spoken word with `python -m pip install -e ".[speech]"`.
|
||||||
|
- If PowerShell refuses to run `Activate.ps1`, run `Set-ExecutionPolicy -Scope CurrentUser RemoteSigned` once, or use `.\.venv\Scripts\activate.bat` in `cmd`.
|
||||||
|
- Leave the environment with `deactivate`. Delete the `.venv` folder to start over.
|
||||||
|
|
||||||
|
## ▶️ Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m flash_card
|
||||||
|
```
|
||||||
|
|
||||||
|
The program is run from the activated `.venv`. The window opens with the first card; the program is built step by step and the pull requests and milestones show each step.
|
||||||
|
|
||||||
|
## 🧪 Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m pytest
|
||||||
|
```
|
||||||
|
|
||||||
|
Quality checks used by the project:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m ruff check src tests
|
||||||
|
python -m mypy
|
||||||
|
lint-imports
|
||||||
|
```
|
||||||
|
|
||||||
|
The window test in `tests/e2e/` is skipped when no display is available.
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
|
||||||
|
GNU Affero General Public License v3.0 or later. See [LICENSE][license].
|
||||||
|
|
||||||
|
## 🔗 Links
|
||||||
|
|
||||||
|
- [Repository][repository]
|
||||||
|
- [Documentation][documentation]
|
||||||
|
- [Issue tracker][issues]
|
||||||
|
|
||||||
|
## 📎 Appendix: Project documents
|
||||||
|
|
||||||
|
The documents a newcomer needs, in reading order.
|
||||||
|
|
||||||
|
| Document | What it tells you |
|
||||||
|
| --- | --- |
|
||||||
|
| [AGENTS.md][agents] | The rules AI agents follow in this repository |
|
||||||
|
| [Business Case][BC-001] | Why the project exists, its scope and success criteria |
|
||||||
|
| [Stakeholder Analysis][SA-001] | Who is involved; the stakeholder IDs used for owners and reviewers |
|
||||||
|
| [Project Plan][PP-001] | The gateways and their schedule |
|
||||||
|
| [Milestones][milestones] | One document per gateway: Go/No-Go criteria and tasks |
|
||||||
|
| [Artifact registry][registry] | Where each artifact type lives, the next version, the PO language and domain |
|
||||||
|
| [Domain dictionary][DICT-001] | The PO terms and the IT terms |
|
||||||
|
| [Traceability matrix][TM-001] | How the artifacts link and when each was last reviewed |
|
||||||
|
| [Framework README][framework-readme] | The framework this project uses |
|
||||||
|
| [Framework changelog][framework-changelog] | What changed in each framework release |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[license]: ./LICENSE
|
||||||
|
[repository]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card
|
||||||
|
[documentation]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/wiki
|
||||||
|
[issues]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/issues
|
||||||
|
[agents]: ./AGENTS.md
|
||||||
|
[BC-001]: ./docs/business-case.md
|
||||||
|
[SA-001]: ./docs/stakeholder-analysis.md
|
||||||
|
[PP-001]: ./docs/project-plan.md
|
||||||
|
[milestones]: ./docs/milestones/
|
||||||
|
[registry]: ./docs/artifact-registry.md
|
||||||
|
[DICT-001]: ./docs/dictionary.md
|
||||||
|
[TM-001]: ./docs/sqa/traceability-matrix.md
|
||||||
|
[framework-readme]: ./framework/README.md
|
||||||
|
[framework-changelog]: ./framework/CHANGELOG.md
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Data
|
||||||
|
|
||||||
|
| File | Content | In git |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `ms-da.csv` | The word list: 1000 of the most frequent Malay words with a Danish translation | yes |
|
||||||
|
| `words_to_learn.csv` | The words you have not marked as known; created by the program | no (`.gitignore`) |
|
||||||
|
|
||||||
|
## Format
|
||||||
|
|
||||||
|
- UTF-8 text, one card per line, cells separated by a semicolon (`;`).
|
||||||
|
- The first line is a header with the two language codes: `ms;da` (Malay, Danish).
|
||||||
|
- Blank lines are ignored.
|
||||||
|
- `words_to_learn.csv` has the same format, so it can be used as a word list.
|
||||||
|
|
||||||
|
## Origin
|
||||||
|
|
||||||
|
The Malay words are a frequency list built from film and series subtitles (the *FrequencyWords* lists by the GitHub user `hermitdave`). The Danish column was filled with machine translation in a spreadsheet, as the course suggests. A word can therefore have a translation that does not fit its use; edit the file if you find one.
|
||||||
|
|
||||||
|
## Start again
|
||||||
|
|
||||||
|
Delete `words_to_learn.csv`. The next start uses `ms-da.csv`.
|
||||||
+1001
File diff suppressed because it is too large
Load Diff
@@ -25,7 +25,7 @@ document of a type. `Primary File` may contain a glob (e.g.
|
|||||||
| DCD | Design Class Diagram | docs/dcd.md | 002 |
|
| DCD | Design Class Diagram | docs/dcd.md | 002 |
|
||||||
| DICT | Domain Dictionary | docs/dictionary.md | 002 |
|
| DICT | Domain Dictionary | docs/dictionary.md | 002 |
|
||||||
| ADR | Architecture Decision Record | docs/adr/adr-NNNN-*.md | 0004 |
|
| ADR | Architecture Decision Record | docs/adr/adr-NNNN-*.md | 0004 |
|
||||||
| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 023 |
|
| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 024 |
|
||||||
| TM | Traceability Matrix | docs/sqa/traceability-matrix.md | 002 |
|
| TM | Traceability Matrix | docs/sqa/traceability-matrix.md | 002 |
|
||||||
|
|
||||||
## Languages
|
## Languages
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# RC-023: Review of PY-MIL-001
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
| Key | Value |
|
||||||
|
| --- | --- |
|
||||||
|
| ID | RC-023 |
|
||||||
|
| CrossReference | [PY-MIL-001], [QC-PY-001] |
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
| Date | Status | Author | Reviewer | Change | Commit |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version | pending |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Artifact Under Review
|
||||||
|
|
||||||
|
- Instance reviewed: [PY-MIL-001]
|
||||||
|
- Checklist used: [QC-PY-001]
|
||||||
|
- Scope: full review
|
||||||
|
- Language and domain: n/a (technical type)
|
||||||
|
- Language reviewer: none
|
||||||
|
- Mode: autonomous, not independent (reviewer is `AI as stand-in for S01`)
|
||||||
|
|
||||||
|
## Checklist Results
|
||||||
|
|
||||||
|
| # | Criterion | Status | Evidence/Notes |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 1 | Packages, modules, functions, variables, classes and constants follow PEP 8 casing (`snake_case`, `PascalCase`, `UPPER_SNAKE`) | Pass | Modules, functions and constants use snake_case and UPPER_SNAKE; ruff rule set N passes. |
|
||||||
|
| 2 | Names state purpose in the domain's language; no unexplained abbreviations, no single-letter names outside tiny scopes | Pass | Names state purpose (`FLIP_DELAY_MS`, `DEFAULT_DATA_DIR`); no abbreviations. |
|
||||||
|
| 3 | Code is produced by the project's formatter and passes its linter with no unexplained suppressions | Pass | `ruff check` and `ruff format --check` pass with no suppressions. |
|
||||||
|
| 4 | Every function and method signature is type-annotated, including `-> None` | Pass | Every function has annotated parameters and return type; `mypy --strict` passes on 9 files. |
|
||||||
|
| 5 | No bare `except:`, no swallowed exceptions; specific exceptions are raised and the cause is kept (`raise ... from`) | Pass | No exception handling exists yet; no bare except. |
|
||||||
|
| 6 | No mutable default arguments and no shadowed builtins | Pass | No mutable defaults, no builtin shadowed. |
|
||||||
|
| 7 | Files, locks and connections are managed with context managers | N-A | No file or connection is opened yet. |
|
||||||
|
| 8 | Public modules, classes and functions have docstrings that say what, not how | Pass | Every module and function has a Doxygen docstring with `@file`/`@brief`/`@param`/`@return`; `doxygen Doxyfile` ends with no warning. |
|
||||||
|
| 9 | Logging uses `logging`, not `print`; no secrets or personal data in log output | Pass | No `print`; no logging needed yet. |
|
||||||
|
| 10 | Classes and operations trace to the Design Class Diagram they implement; deviations are recorded | Pass | `app.main` and `app.run` are the `app.main` of DCD-001; constants have no design class. |
|
||||||
|
| 11 | Tests exist for new behaviour, are named for the behaviour, and do not depend on order or the network | Pass | `tests/unit/test_constants.py` and `test_app.py` (7 tests) name the behaviour and use no network. |
|
||||||
|
| 12 | Type checker runs in strict mode without errors; `Any` is justified in a comment | Pass | `mypy --strict` reports no issue; no `Any` is used. |
|
||||||
|
| 13 | Dependencies are declared and pinned in the project's dependency file, none unused | Pass | `dependencies` is empty; extras are declared in `pyproject.toml` with version ranges, and `playsound` is pinned. |
|
||||||
|
| 14 | Each piece of knowledge (a business rule, constant, format, validation or query) is defined in one place; code is merged only where it expresses the same knowledge, not where it merely looks alike | Pass | Every constant is defined once in `constants.py`. |
|
||||||
|
| 15 | Imports point inward: business-rule modules import no framework, database, UI or delivery-mechanism code, and there are no import cycles | Pass | `lint-imports` reports the contract `Layers point inward` kept. |
|
||||||
|
| 16 | SOLID holds: a class or module has one reason to change; new behaviour is added by extension, not by editing a type test in several places; subclasses honour the contract of their base; protocols are narrow; high-level code depends on protocols or abstract types and receives its implementations | Pass | Modules are small and single-purpose; nothing to extend yet. |
|
||||||
|
| 17 | Source is laid out in the layers and folders of the clean architecture folder structure (`domain`, `application`, `infrastructure`, `presentation` packages under `src/`, and `unit`, `integration` and `e2e` under `tests/`), in `snake_case`, and imports between the layers point inward | Pass | `domain`, `application`, `infrastructure`, `presentation` packages exist under `src/flash_card/`, and `tests/unit`, `integration`, `e2e` exist. |
|
||||||
|
| 18 | File and folder names are English, contain no space and no character outside letters, digits, `-`, `_` and `.`, and follow the casing of the language for files | Pass | File and folder names are English lower snake_case with no space. |
|
||||||
|
|
||||||
|
## Overall Verdict
|
||||||
|
|
||||||
|
Go — every Mandatory criterion passes. Reviewed by AI as stand-in for S01; not independent of the author.
|
||||||
|
|
||||||
|
Milestone MIL-001 Go criteria: 1 `pip install -e ".[dev]"` succeeded in a fresh `.venv`; 2 `git check-ignore` reports `.venv/`, `.env`, `data/words_to_learn.csv`, `docs/doxygen/`; 3 packages and test folders exist; 4 `doxygen Doxyfile` exit 0 with no warning; 5 `check-readme.sh` exit 0; 6 `pytest` 7 passed. All six Go.
|
||||||
|
|
||||||
|
## Action Items
|
||||||
|
|
||||||
|
| Action | Owner | Due |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| none | - | - |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[PY-MIL-001]: ../../../src/flash_card
|
||||||
|
[QC-PY-001]: ../../../framework/qc/code/qc-programming-python.md
|
||||||
@@ -45,6 +45,7 @@ updated whenever an artifact instance is created or reviewed.
|
|||||||
| MIL-005 | Milestone / Gateway | en | it | BC-001, PP-001 | tasks of MIL-005 | [RC-020] |
|
| MIL-005 | Milestone / Gateway | en | it | BC-001, PP-001 | tasks of MIL-005 | [RC-020] |
|
||||||
| MIL-006 | Milestone / Gateway | en | it | BC-001, PP-001 | tasks of MIL-006 | [RC-021] |
|
| MIL-006 | Milestone / Gateway | en | it | BC-001, PP-001 | tasks of MIL-006 | [RC-021] |
|
||||||
| MIL-007 | Milestone / Gateway | en | it | BC-001, PP-001 | tasks of MIL-007 | [RC-022] |
|
| MIL-007 | Milestone / Gateway | en | it | BC-001, PP-001 | tasks of MIL-007 | [RC-022] |
|
||||||
|
| PY-MIL-001 | Python Source Code | - | - | DCD-001, ADR-0002 | - | [RC-023] |
|
||||||
|
|
||||||
## Coverage Notes
|
## Coverage Notes
|
||||||
|
|
||||||
@@ -76,6 +77,7 @@ updated whenever an artifact instance is created or reviewed.
|
|||||||
[MIL-005]: ../milestones/mil-005-step4-save-progress.md
|
[MIL-005]: ../milestones/mil-005-step4-save-progress.md
|
||||||
[MIL-006]: ../milestones/mil-006-malay-speech.md
|
[MIL-006]: ../milestones/mil-006-malay-speech.md
|
||||||
[MIL-007]: ../milestones/mil-007-documentation-and-publication.md
|
[MIL-007]: ../milestones/mil-007-documentation-and-publication.md
|
||||||
|
[PY-MIL-001]: ../../src/flash_card
|
||||||
[RC-001]: ./reviews/rc-001-business-case.md
|
[RC-001]: ./reviews/rc-001-business-case.md
|
||||||
[RC-002]: ./reviews/rc-002-stakeholder-analysis.md
|
[RC-002]: ./reviews/rc-002-stakeholder-analysis.md
|
||||||
[RC-003]: ./reviews/rc-003-project-plan.md
|
[RC-003]: ./reviews/rc-003-project-plan.md
|
||||||
@@ -98,4 +100,5 @@ updated whenever an artifact instance is created or reviewed.
|
|||||||
[RC-020]: ./reviews/rc-020-mil-005.md
|
[RC-020]: ./reviews/rc-020-mil-005.md
|
||||||
[RC-021]: ./reviews/rc-021-mil-006.md
|
[RC-021]: ./reviews/rc-021-mil-006.md
|
||||||
[RC-022]: ./reviews/rc-022-mil-007.md
|
[RC-022]: ./reviews/rc-022-mil-007.md
|
||||||
|
[RC-023]: ./reviews/rc-023-py-mil-001.md
|
||||||
[c3fbcbc]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/c3fbcbcfaf799e0ae34aed7e14d3bc8a62f03db3
|
[c3fbcbc]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/031-flash-card/commit/c3fbcbcfaf799e0ae34aed7e14d3bc8a62f03db3
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=68"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "flash-card"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Flash card program for learning Malay words with Danish translations (Udemy 100 Days of Code, day 31)"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.13"
|
||||||
|
license = "AGPL-3.0-or-later"
|
||||||
|
license-files = ["LICENSE"]
|
||||||
|
authors = [{ name = "Jens Tirsvad Nielsen" }]
|
||||||
|
keywords = ["flashcards", "tkinter", "language-learning", "malay", "danish", "udemy"]
|
||||||
|
classifiers = [
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"Programming Language :: Python :: 3.13",
|
||||||
|
"Environment :: X11 Applications",
|
||||||
|
"Topic :: Education",
|
||||||
|
]
|
||||||
|
# The core program uses the Python standard library only (ADR-0001).
|
||||||
|
dependencies = []
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
# Spoken Malay word (text to speech). Optional; the program works without it.
|
||||||
|
speech = ["gTTS>=2.5", "playsound==1.2.2"]
|
||||||
|
dev = ["pytest>=8", "ruff>=0.6", "mypy>=1.11", "import-linter>=2.0"]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
flash-card = "flash_card.presentation.app:run"
|
||||||
|
|
||||||
|
[tool.setuptools.packages.find]
|
||||||
|
where = ["src"]
|
||||||
|
|
||||||
|
[tool.setuptools.package-data]
|
||||||
|
flash_card = ["presentation/assets/*.png"]
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
testpaths = ["tests"]
|
||||||
|
pythonpath = ["src"]
|
||||||
|
addopts = "-ra"
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
line-length = 88
|
||||||
|
target-version = "py313"
|
||||||
|
src = ["src", "tests"]
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
select = ["E", "F", "I", "UP", "B", "SIM", "N", "D"]
|
||||||
|
ignore = ["D203", "D213"]
|
||||||
|
|
||||||
|
[tool.ruff.lint.per-file-ignores]
|
||||||
|
"tests/**" = ["D"]
|
||||||
|
|
||||||
|
[tool.ruff.lint.pydocstyle]
|
||||||
|
convention = "pep257"
|
||||||
|
|
||||||
|
[tool.mypy]
|
||||||
|
python_version = "3.13"
|
||||||
|
strict = true
|
||||||
|
mypy_path = ["src"]
|
||||||
|
packages = ["flash_card"]
|
||||||
|
|
||||||
|
[tool.importlinter]
|
||||||
|
root_package = "flash_card"
|
||||||
|
|
||||||
|
[[tool.importlinter.contracts]]
|
||||||
|
name = "Layers point inward"
|
||||||
|
type = "layers"
|
||||||
|
layers = [
|
||||||
|
"flash_card.presentation",
|
||||||
|
"flash_card.infrastructure",
|
||||||
|
"flash_card.application",
|
||||||
|
"flash_card.domain",
|
||||||
|
]
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
"""Flash card program that teaches Malay words with Danish translations.
|
||||||
|
|
||||||
|
@file __init__.py
|
||||||
|
@brief Package root of the flash card program.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__version__ = "0.1.0"
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
"""Run the flash card program with ``python -m flash_card``.
|
||||||
|
|
||||||
|
@file __main__.py
|
||||||
|
@brief Module entry point that calls the presentation layer.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from flash_card.presentation.app import run
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
run()
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
"""Application layer: use cases and ports.
|
||||||
|
|
||||||
|
@file __init__.py
|
||||||
|
@brief Holds use cases and the ports they need; may import only the domain layer.
|
||||||
|
"""
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
"""All constants of the flash card program, defined once.
|
||||||
|
|
||||||
|
@file constants.py
|
||||||
|
@brief Window, card, font, language, file and asset constants.
|
||||||
|
Imports only the standard library.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from collections.abc import Mapping
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Final
|
||||||
|
|
||||||
|
## @brief Window background colour.
|
||||||
|
BACKGROUND_COLOR: Final[str] = "#B1DDC6"
|
||||||
|
## @brief Window title.
|
||||||
|
WINDOW_TITLE: Final[str] = "Flashy"
|
||||||
|
## @brief Padding around the card in pixels.
|
||||||
|
WINDOW_PADDING: Final[int] = 50
|
||||||
|
|
||||||
|
## @brief Card width in pixels.
|
||||||
|
CARD_WIDTH: Final[int] = 800
|
||||||
|
## @brief Card height in pixels.
|
||||||
|
CARD_HEIGHT: Final[int] = 526
|
||||||
|
## @brief Horizontal centre of the card in pixels.
|
||||||
|
CARD_CENTER_X: Final[int] = 400
|
||||||
|
## @brief Vertical position of the language text in pixels.
|
||||||
|
LANGUAGE_TEXT_Y: Final[int] = 150
|
||||||
|
## @brief Vertical position of the word text in pixels.
|
||||||
|
WORD_TEXT_Y: Final[int] = 263
|
||||||
|
## @brief Delay before the card flips, in milliseconds.
|
||||||
|
FLIP_DELAY_MS: Final[int] = 3000
|
||||||
|
|
||||||
|
## @brief Font of the language text: family, size, style.
|
||||||
|
LANGUAGE_FONT: Final[tuple[str, int, str]] = ("Arial", 40, "italic")
|
||||||
|
## @brief Font of the word text: family, size, style.
|
||||||
|
WORD_FONT: Final[tuple[str, int, str]] = ("Arial", 60, "bold")
|
||||||
|
## @brief Text colour on the card front.
|
||||||
|
FRONT_TEXT_COLOR: Final[str] = "black"
|
||||||
|
## @brief Text colour on the card back.
|
||||||
|
BACK_TEXT_COLOR: Final[str] = "white"
|
||||||
|
|
||||||
|
## @brief Display names by language code; the caller shows unknown codes in upper case.
|
||||||
|
LANGUAGE_NAMES: Final[Mapping[str, str]] = {"ms": "Malay", "da": "Danish"}
|
||||||
|
## @brief Message shown when no word is left to learn.
|
||||||
|
FINISHED_MESSAGE: Final[str] = "Nothing left to learn"
|
||||||
|
## @brief Language name shown before the first card.
|
||||||
|
PLACEHOLDER_LANGUAGE_NAME: Final[str] = "Malay"
|
||||||
|
## @brief Word shown before the first card.
|
||||||
|
PLACEHOLDER_WORD: Final[str] = "kata"
|
||||||
|
|
||||||
|
## @brief Delimiter of the CSV files.
|
||||||
|
CSV_DELIMITER: Final[str] = ";"
|
||||||
|
## @brief Encoding of the CSV files.
|
||||||
|
CSV_ENCODING: Final[str] = "utf-8"
|
||||||
|
## @brief File name of the vocabulary.
|
||||||
|
VOCABULARY_FILE_NAME: Final[str] = "ms-da.csv"
|
||||||
|
## @brief File name of the words still to learn.
|
||||||
|
PROGRESS_FILE_NAME: Final[str] = "words_to_learn.csv"
|
||||||
|
## @brief Language code used for speech.
|
||||||
|
SPEECH_LANGUAGE_CODE: Final[str] = "ms"
|
||||||
|
|
||||||
|
## @brief Root folder of the project.
|
||||||
|
PROJECT_ROOT: Final[Path] = Path(__file__).resolve().parents[2]
|
||||||
|
## @brief Default folder of the data files.
|
||||||
|
DEFAULT_DATA_DIR: Final[Path] = PROJECT_ROOT / "data"
|
||||||
|
## @brief Folder of the image assets.
|
||||||
|
ASSETS_DIR: Final[Path] = Path(__file__).resolve().parent / "presentation" / "assets"
|
||||||
|
## @brief File name of the card front image.
|
||||||
|
CARD_FRONT_IMAGE_NAME: Final[str] = "card_front.png"
|
||||||
|
## @brief File name of the card back image.
|
||||||
|
CARD_BACK_IMAGE_NAME: Final[str] = "card_back.png"
|
||||||
|
## @brief File name of the right button image.
|
||||||
|
RIGHT_IMAGE_NAME: Final[str] = "right.png"
|
||||||
|
## @brief File name of the wrong button image.
|
||||||
|
WRONG_IMAGE_NAME: Final[str] = "wrong.png"
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
"""Domain layer: entities and rules of the flash card program.
|
||||||
|
|
||||||
|
@file __init__.py
|
||||||
|
@brief Holds entities, value objects and rules.
|
||||||
|
May import nothing outside the standard library.
|
||||||
|
"""
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
"""Infrastructure layer: adapters for files, speech and other outside systems.
|
||||||
|
|
||||||
|
@file __init__.py
|
||||||
|
@brief Holds implementations of ports; may import the application and domain layers.
|
||||||
|
"""
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
"""Presentation layer: views, view models and the entry point.
|
||||||
|
|
||||||
|
@file __init__.py
|
||||||
|
@brief Holds the user interface and the composition root; may import every inner layer.
|
||||||
|
"""
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
"""Entry point of the flash card program.
|
||||||
|
|
||||||
|
@file app.py
|
||||||
|
@brief Starts the program; the window arrives in MIL-002.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from collections.abc import Sequence
|
||||||
|
from typing import NoReturn
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: Sequence[str] | None = None) -> int:
|
||||||
|
"""Run the program and return its exit code.
|
||||||
|
|
||||||
|
The window arrives in MIL-002; for now nothing happens.
|
||||||
|
|
||||||
|
@param argv Command line arguments, or None for none.
|
||||||
|
@return The exit code, 0 on success.
|
||||||
|
"""
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def run() -> NoReturn:
|
||||||
|
"""Run the program and exit the interpreter with its exit code.
|
||||||
|
|
||||||
|
@return Never returns; the function always raises SystemExit.
|
||||||
|
@throws SystemExit Always, with the exit code of main.
|
||||||
|
"""
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
"""Tests of the entry point.
|
||||||
|
|
||||||
|
@file test_app.py
|
||||||
|
@brief Checks main of flash_card.presentation.app.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from flash_card.presentation.app import main
|
||||||
|
|
||||||
|
|
||||||
|
def test_main_returns_zero_without_arguments() -> None:
|
||||||
|
assert main([]) == 0
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
"""Tests of the constants module.
|
||||||
|
|
||||||
|
@file test_constants.py
|
||||||
|
@brief Checks the key values of flash_card.constants.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from flash_card import constants
|
||||||
|
|
||||||
|
|
||||||
|
def test_flip_delay_ms_is_three_seconds() -> None:
|
||||||
|
assert constants.FLIP_DELAY_MS == 3000
|
||||||
|
|
||||||
|
|
||||||
|
def test_background_color_is_green() -> None:
|
||||||
|
assert constants.BACKGROUND_COLOR == "#B1DDC6"
|
||||||
|
|
||||||
|
|
||||||
|
def test_default_data_dir_ends_with_data() -> None:
|
||||||
|
assert constants.DEFAULT_DATA_DIR.name == "data"
|
||||||
|
|
||||||
|
|
||||||
|
def test_file_names_are_csv() -> None:
|
||||||
|
assert constants.VOCABULARY_FILE_NAME.endswith(".csv")
|
||||||
|
assert constants.PROGRESS_FILE_NAME.endswith(".csv")
|
||||||
|
|
||||||
|
|
||||||
|
def test_language_names_cover_ms_and_da() -> None:
|
||||||
|
assert "ms" in constants.LANGUAGE_NAMES
|
||||||
|
assert "da" in constants.LANGUAGE_NAMES
|
||||||
|
|
||||||
|
|
||||||
|
def test_project_root_contains_pyproject() -> None:
|
||||||
|
assert (constants.PROJECT_ROOT / "pyproject.toml").is_file()
|
||||||
Reference in New Issue
Block a user