Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 12 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

[![CI](https://github.com/DataTideHH/flask-country-data-api/actions/workflows/ci.yml/badge.svg)](https://github.com/DataTideHH/flask-country-data-api/actions/workflows/ci.yml)

[Project site](https://datatidehh.github.io/flask-country-data-api/) · [OpenAPI contract](openapi/openapi.yaml) · [Architecture](docs/architecture.md)

**Reproducible World Bank ingestion workflow with source validation, constrained SQLite persistence, versioned Flask endpoints, SQL data-quality checks, OpenAPI documentation and cross-platform automated tests.**

## Portfolio purpose
Expand Down Expand Up @@ -40,8 +42,8 @@ Normal HTTP requests never call the World Bank directly. Data acquisition is an
| Process observability | `ingestion_runs` records start, completion, status, row counts and failures |
| SQL / Data quality | Ten named checks executed from `sql/data_quality_queries.sql` |
| API design | Versioned endpoints, bounded parameters and consistent JSON contracts |
| Documentation | OpenAPI 3.1, architecture diagram, ERD, data dictionary and provenance mapping |
| Automated validation | Unit, persistence, route, CLI, quality and contract tests on Ubuntu and Windows |
| Documentation | OpenAPI 3.1, architecture diagram, ERD, data dictionary, provenance mapping and a compact project site |
| Automated validation | Unit, persistence, route, CLI, quality and validated OpenAPI contract tests on Ubuntu and Windows |

## Architecture

Expand Down Expand Up @@ -100,7 +102,7 @@ GET /api/v1/data-quality
GET /api/v1/ingestion-runs?limit=5
```

The complete contract is versioned in [`openapi/openapi.yaml`](openapi/openapi.yaml).
The complete contract is versioned in [`openapi/openapi.yaml`](openapi/openapi.yaml). CI validates both OpenAPI semantics and coverage of the implemented Flask routes.

## Summary response

Expand Down Expand Up @@ -224,7 +226,7 @@ python -m flask --app country_api:create_app refresh-data \
--to-year 2024
```

A failed live refresh is recorded in `ingestion_runs` and does not replace previously committed data.
A failed live refresh is recorded in `ingestion_runs`. Data replacement itself is transactional, so validation or persistence failures do not partially replace the selected country and year range.

## Tests and CI

Expand All @@ -245,8 +247,9 @@ GitHub Actions validates Python 3.12 on Ubuntu and Windows. The workflow:

1. compiles application and test modules
2. runs unit, persistence, route, CLI, data-quality and OpenAPI tests
3. builds a deterministic SQLite database from fixtures
4. executes the persisted SQL quality report and requires `passed`
3. validates the OpenAPI 3.1 document and its coverage of runtime routes
4. builds a deterministic SQLite database from fixtures
5. executes the persisted SQL quality report and requires `passed`

Automated tests never perform live HTTP requests.

Expand All @@ -256,18 +259,11 @@ Automated tests never perform live HTTP requests.
flask-country-data-api/
├── .github/workflows/ci.yml
├── country_api/
│ ├── __init__.py
│ ├── cli.py
│ ├── database.py
│ ├── errors.py
│ ├── routes.py
│ ├── service.py
│ ├── validation.py
│ └── world_bank.py
├── data/fixtures/
│ ├── world-bank-countries.json
│ └── world-bank-population.json
├── docs/
│ ├── _config.yml
│ ├── index.md
│ ├── assets/css/style.scss
│ ├── architecture.md
│ ├── data-dictionary.md
│ ├── data-model.md
Expand All @@ -278,11 +274,6 @@ flask-country-data-api/
│ ├── data_quality_queries.sql
│ └── schema.sql
├── tests/
│ ├── test_openapi.py
│ ├── test_reporting.py
│ ├── test_routes_and_cli.py
│ ├── test_service_and_database.py
│ └── test_validation.py
├── main.py
├── requirements-dev.txt
├── requirements.txt
Expand Down
15 changes: 15 additions & 0 deletions docs/_config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
title: Flask Country Data API
description: Reproducible World Bank ingestion, SQLite persistence, SQL data-quality checks and a documented Flask read API.
theme: jekyll-theme-cayman
show_downloads: false
repository: DataTideHH/flask-country-data-api
url: https://datatidehh.github.io
baseurl: /flask-country-data-api
lang: en

plugins:
- jekyll-relative-links

relative_links:
enabled: true
collections: true
291 changes: 291 additions & 0 deletions docs/assets/css/style.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,291 @@
---
---

@import "{{ site.theme }}";

:root {
--hanse-blue: #0a2f4f;
--hanse-blue-deep: #07243c;
--tide-teal: #0b92b6;
--tide-teal-light: #43c4df;
--paper: #fafaf7;
--surface: #ffffff;
--ink: #0b1820;
--ink-muted: #4a5662;
--line: rgba(11, 24, 32, 0.12);
--code-bg: #eef4f5;
--shadow: 0 18px 50px rgba(10, 47, 79, 0.08);

--brand-sans: "Manrope", -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif;
--brand-display: "Montserrat", -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif;
--brand-mono: ui-monospace, "SFMono-Regular", "SF Mono", "Cascadia Mono", Menlo, Consolas, monospace;
}

html {
background: var(--paper);
}

body {
background: var(--paper);
color: var(--ink);
font-family: var(--brand-sans);
font-size: 17px;
line-height: 1.65;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}

.page-header {
position: relative;
overflow: hidden;
color: #fff;
background-color: var(--hanse-blue);
background-image:
radial-gradient(circle at 12% 18%, rgba(67, 196, 223, 0.22), transparent 34%),
radial-gradient(circle at 88% 88%, rgba(255, 255, 255, 0.10), transparent 33%),
linear-gradient(135deg, var(--hanse-blue-deep) 0%, var(--hanse-blue) 52%, var(--tide-teal) 100%);
border-bottom: 4px solid var(--tide-teal);
}

.page-header::after {
content: "";
position: absolute;
left: -5%;
right: -5%;
bottom: -62px;
height: 112px;
opacity: 0.34;
pointer-events: none;
background:
radial-gradient(75% 90% at 20% 0%, rgba(255, 255, 255, 0.22) 0 49%, transparent 50%),
radial-gradient(75% 90% at 72% 10%, rgba(67, 196, 223, 0.24) 0 49%, transparent 50%);
}

.project-name,
.main-content h1,
.main-content h2,
.main-content h3,
.main-content h4 {
font-family: var(--brand-display);
}

.project-name {
position: relative;
z-index: 1;
font-weight: 800;
letter-spacing: -0.025em;
}

.project-tagline {
position: relative;
z-index: 1;
max-width: 54rem;
margin-right: auto;
margin-left: auto;
color: rgba(255, 255, 255, 0.84);
font-weight: 400;
}

.page-header .btn {
position: relative;
z-index: 1;
border-color: rgba(255, 255, 255, 0.34);
background: rgba(255, 255, 255, 0.08);
color: #fff;
backdrop-filter: blur(6px);
}

.page-header .btn:hover,
.page-header .btn:focus {
border-color: rgba(255, 255, 255, 0.68);
background: rgba(255, 255, 255, 0.16);
}

.main-content {
color: var(--ink);
}

.main-content h1,
.main-content h2,
.main-content h3,
.main-content h4 {
color: var(--hanse-blue);
font-weight: 700;
letter-spacing: -0.015em;
}

.main-content h2 {
margin-top: 2.4rem;
padding-bottom: 0.45rem;
border-bottom: 1px solid var(--line);
}

.main-content a {
color: var(--tide-teal);
font-weight: 600;
Comment on lines +123 to +125

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Darken light-mode links to preserve text contrast

In the default light color scheme, #0b92b6 has only about a 3.46:1 contrast ratio against the page's #fafaf7 background (and 3.62:1 against white table surfaces). The normal 17px link text therefore falls below the WCAG AA 4.5:1 threshold, making links difficult to read for low-vision users; use a darker link color in light mode while retaining the brighter dark-mode override.

Useful? React with 👍 / 👎.

text-decoration-thickness: 0.08em;
text-underline-offset: 0.18em;
}

.main-content a:hover,
.main-content a:focus {
color: var(--hanse-blue);
}

a:focus-visible,
.btn:focus-visible {
outline: 3px solid var(--tide-teal-light);
outline-offset: 3px;
}

.main-content code {
color: var(--hanse-blue);
background-color: var(--code-bg);
font-family: var(--brand-mono);
border: 1px solid var(--line);
border-radius: 0.3rem;
}

.main-content pre {
background: var(--surface);
border: 1px solid var(--line);
border-left: 4px solid var(--tide-teal);
border-radius: 0.55rem;
box-shadow: var(--shadow);
}

.main-content pre code {
color: var(--ink);
background: transparent;
border: 0;
}

.main-content blockquote {
color: var(--ink-muted);
border-left: 0.35rem solid var(--tide-teal);
background: rgba(11, 146, 182, 0.055);
padding: 0.8rem 1.1rem;
border-radius: 0 0.45rem 0.45rem 0;
}

.main-content table {
display: table;
width: 100%;
background: var(--surface);
border-collapse: collapse;
box-shadow: 0 8px 28px rgba(10, 47, 79, 0.045);
}

.main-content table th {
color: #fff;
background: var(--hanse-blue);
font-family: var(--brand-display);
font-weight: 700;
}

.main-content table th,
.main-content table td {
border-color: var(--line);
}

.main-content table tr:nth-child(even) td {
background: rgba(11, 146, 182, 0.035);
}

.main-content hr {
height: 1px;
background: var(--line);
}

.site-footer {
color: var(--ink-muted);
border-top-color: var(--line);
}

.site-footer-owner,
.site-footer-credits {
color: var(--ink-muted);
}

::selection {
color: var(--ink);
background: rgba(67, 196, 223, 0.34);
}

@media screen and (min-width: 64em) {
.page-header {
padding: 4.8rem 6rem 5.2rem;
}

.main-content {
max-width: 68rem;
padding-top: 3rem;
}
}

@media screen and (max-width: 42em) {
body {
font-size: 16px;
}

.page-header {
padding-bottom: 3.2rem;
}

.main-content table {
display: block;
overflow-x: auto;
}
}

@media (prefers-color-scheme: dark) {
:root {
--hanse-blue: #6fb4dc;
--hanse-blue-deep: #07131d;
--tide-teal: #2dc3e8;
--tide-teal-light: #7bdcf0;
--paper: #0a1620;
--surface: #102330;
--ink: #eef2f5;
--ink-muted: #9aa7b4;
--line: rgba(238, 242, 245, 0.14);
--code-bg: #132a38;
--shadow: 0 18px 50px rgba(0, 0, 0, 0.20);
}

.page-header {
background-image:
radial-gradient(circle at 12% 18%, rgba(45, 195, 232, 0.19), transparent 34%),
radial-gradient(circle at 88% 88%, rgba(255, 255, 255, 0.07), transparent 33%),
linear-gradient(135deg, #06111a 0%, #0a2f4f 58%, #08748f 100%);
}

.main-content h1,
.main-content h2,
.main-content h3,
.main-content h4 {
color: var(--hanse-blue);
}

.main-content table th {
color: #fff;
background: #0a2f4f;
}

.main-content table tr:nth-child(even) td {
background: rgba(45, 195, 232, 0.04);
}

.main-content blockquote {
background: rgba(45, 195, 232, 0.065);
}
}

@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
scroll-behavior: auto !important;
transition: none !important;
}
}
Loading
Loading