apographe 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- apographe-0.1.0/.gitignore +172 -0
- apographe-0.1.0/CHANGELOG.md +27 -0
- apographe-0.1.0/LICENSE +21 -0
- apographe-0.1.0/PKG-INFO +117 -0
- apographe-0.1.0/README.md +85 -0
- apographe-0.1.0/docs/adding-a-source.md +59 -0
- apographe-0.1.0/docs/library.md +59 -0
- apographe-0.1.0/docs/scheduling.md +49 -0
- apographe-0.1.0/docs/sources/cy-ekalathi.md +72 -0
- apographe-0.1.0/docs/sources/cy-fuel.md +93 -0
- apographe-0.1.0/docs/usage.md +65 -0
- apographe-0.1.0/pyproject.toml +78 -0
- apographe-0.1.0/src/apographe/__init__.py +37 -0
- apographe-0.1.0/src/apographe/__main__.py +5 -0
- apographe-0.1.0/src/apographe/_version.py +8 -0
- apographe-0.1.0/src/apographe/cli.py +208 -0
- apographe-0.1.0/src/apographe/core/__init__.py +1 -0
- apographe-0.1.0/src/apographe/core/http.py +94 -0
- apographe-0.1.0/src/apographe/core/oauth.py +83 -0
- apographe-0.1.0/src/apographe/core/registry.py +33 -0
- apographe-0.1.0/src/apographe/core/source.py +82 -0
- apographe-0.1.0/src/apographe/core/storage.py +121 -0
- apographe-0.1.0/src/apographe/py.typed +0 -0
- apographe-0.1.0/src/apographe/runner.py +131 -0
- apographe-0.1.0/src/apographe/sources/__init__.py +0 -0
- apographe-0.1.0/src/apographe/sources/cy/__init__.py +0 -0
- apographe-0.1.0/src/apographe/sources/cy/ekalathi/__init__.py +197 -0
- apographe-0.1.0/src/apographe/sources/cy/ekalathi/client.py +98 -0
- apographe-0.1.0/src/apographe/sources/cy/ekalathi/parse.py +250 -0
- apographe-0.1.0/src/apographe/sources/cy/fuel/__init__.py +1 -0
- apographe-0.1.0/src/apographe/sources/cy/fuel/api.py +258 -0
- apographe-0.1.0/src/apographe/sources/cy/fuel/pdf.py +346 -0
- apographe-0.1.0/src/apographe/sources/cy/fuel/scraper.py +184 -0
- apographe-0.1.0/src/apographe/sources/cy/fuel/source.py +113 -0
- apographe-0.1.0/tests/__init__.py +0 -0
- apographe-0.1.0/tests/core/__init__.py +0 -0
- apographe-0.1.0/tests/core/test_core.py +104 -0
- apographe-0.1.0/tests/fixtures/ekalathi/categories.json +68 -0
- apographe-0.1.0/tests/fixtures/ekalathi/companies.json +26 -0
- apographe-0.1.0/tests/fixtures/ekalathi/history_2074.json +22 -0
- apographe-0.1.0/tests/fixtures/ekalathi/product_list_page0.json +48 -0
- apographe-0.1.0/tests/fixtures/ekalathi/product_list_page1.json +30 -0
- apographe-0.1.0/tests/fixtures/ekalathi/regions.json +22 -0
- apographe-0.1.0/tests/fixtures/ekalathi/store_prices_2074.json +98 -0
- apographe-0.1.0/tests/fixtures/fuel_api/response_unleaded95.xml +88 -0
- apographe-0.1.0/tests/sources/__init__.py +0 -0
- apographe-0.1.0/tests/sources/cy/__init__.py +0 -0
- apographe-0.1.0/tests/sources/cy/test_ekalathi.py +196 -0
- apographe-0.1.0/tests/sources/cy/test_fuel_api.py +285 -0
- apographe-0.1.0/tests/sources/cy/test_fuel_pdf.py +110 -0
- apographe-0.1.0/tests/sources/cy/test_fuel_scraper.py +94 -0
- apographe-0.1.0/tests/test_env.py +90 -0
- apographe-0.1.0/tests/test_run.py +50 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py,cover
|
|
50
|
+
.hypothesis/
|
|
51
|
+
.pytest_cache/
|
|
52
|
+
cover/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
|
|
74
|
+
# PyBuilder
|
|
75
|
+
.pybuilder/
|
|
76
|
+
target/
|
|
77
|
+
|
|
78
|
+
# Jupyter Notebook
|
|
79
|
+
.ipynb_checkpoints
|
|
80
|
+
|
|
81
|
+
# IPython
|
|
82
|
+
profile_default/
|
|
83
|
+
ipython_config.py
|
|
84
|
+
|
|
85
|
+
# pyenv
|
|
86
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
87
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
88
|
+
# .python-version
|
|
89
|
+
|
|
90
|
+
# pipenv
|
|
91
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
92
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
93
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
94
|
+
# install all needed dependencies.
|
|
95
|
+
#Pipfile.lock
|
|
96
|
+
|
|
97
|
+
# poetry
|
|
98
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
99
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
100
|
+
# commonly ignored for libraries.
|
|
101
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
102
|
+
#poetry.lock
|
|
103
|
+
|
|
104
|
+
# pdm
|
|
105
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
106
|
+
#pdm.lock
|
|
107
|
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
108
|
+
# in version control.
|
|
109
|
+
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
|
110
|
+
.pdm.toml
|
|
111
|
+
.pdm-python
|
|
112
|
+
.pdm-build/
|
|
113
|
+
|
|
114
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
115
|
+
__pypackages__/
|
|
116
|
+
|
|
117
|
+
# Celery stuff
|
|
118
|
+
celerybeat-schedule
|
|
119
|
+
celerybeat.pid
|
|
120
|
+
|
|
121
|
+
# SageMath parsed files
|
|
122
|
+
*.sage.py
|
|
123
|
+
|
|
124
|
+
# Environments
|
|
125
|
+
.env
|
|
126
|
+
.venv
|
|
127
|
+
env/
|
|
128
|
+
venv/
|
|
129
|
+
ENV/
|
|
130
|
+
env.bak/
|
|
131
|
+
venv.bak/
|
|
132
|
+
|
|
133
|
+
# Spyder project settings
|
|
134
|
+
.spyderproject
|
|
135
|
+
.spyproject
|
|
136
|
+
|
|
137
|
+
# Rope project settings
|
|
138
|
+
.ropeproject
|
|
139
|
+
|
|
140
|
+
# mkdocs documentation
|
|
141
|
+
/site
|
|
142
|
+
|
|
143
|
+
# mypy
|
|
144
|
+
.mypy_cache/
|
|
145
|
+
.dmypy.json
|
|
146
|
+
dmypy.json
|
|
147
|
+
|
|
148
|
+
# Pyre type checker
|
|
149
|
+
.pyre/
|
|
150
|
+
|
|
151
|
+
# pytype static type analyzer
|
|
152
|
+
.pytype/
|
|
153
|
+
|
|
154
|
+
# Cython debug symbols
|
|
155
|
+
cython_debug/
|
|
156
|
+
|
|
157
|
+
# PyCharm
|
|
158
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
159
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
160
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
161
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
162
|
+
#.idea/
|
|
163
|
+
|
|
164
|
+
# vscode settings
|
|
165
|
+
.vscode/
|
|
166
|
+
|
|
167
|
+
data/*
|
|
168
|
+
.scratch/
|
|
169
|
+
!data/.gitkeep
|
|
170
|
+
.DS_Store
|
|
171
|
+
|
|
172
|
+
artifacts/*
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 - 2026-10-25
|
|
4
|
+
|
|
5
|
+
First public release. The project began as `fuel_prices_retrieval` (v0.2.0), a
|
|
6
|
+
scraper for Cyprus fuel prices, and grew into a way to collect public data from
|
|
7
|
+
many sources into dated plain files.
|
|
8
|
+
|
|
9
|
+
- Source framework: a `Source` interface, a polite HTTP client, OAuth client
|
|
10
|
+
credentials, atomic CSV and JSON storage.
|
|
11
|
+
- Use it from Python: `from apographe import run` collects a source into dated
|
|
12
|
+
files, and the main classes (`Source`, `Store`, `PoliteClient`, ...) are
|
|
13
|
+
exported.
|
|
14
|
+
- `apographe list`, `apographe info SOURCE` and `apographe run`. `info` shows a
|
|
15
|
+
source's jobs and the environment variables and extras each needs; a job with
|
|
16
|
+
a missing variable stops early and names it. `run --env-file PATH` reads
|
|
17
|
+
`KEY=VALUE` lines for one run.
|
|
18
|
+
- `cy.ekalathi`: Cyprus supermarket prices, every product in every store, daily;
|
|
19
|
+
cheapest-price history since September 2025.
|
|
20
|
+
- `cy.fuel`: Cyprus fuel prices per station.
|
|
21
|
+
- `prices` (default): scrapes the Ministry's website with Chrome. Chrome's
|
|
22
|
+
sandbox stays on except in CI.
|
|
23
|
+
- `api`: the official CY Connect API (OAuth 2.0 client credentials). The
|
|
24
|
+
endpoints and credentials come from environment variables
|
|
25
|
+
(`CYCONNECT_TOKEN_URL`, `CYCONNECT_API_URL`, `CYCONNECT_CLIENT_ID`,
|
|
26
|
+
`CYCONNECT_CLIENT_SECRET`). Sandbox answers go to their own folder.
|
|
27
|
+
- `pdf`: import the observatory page saved as PDFs.
|
apographe-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-2026 Aristides Zenonos
|
|
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.
|
apographe-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: apographe
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Collect public data, source by source and day by day, into plain files.
|
|
5
|
+
Project-URL: Homepage, https://github.com/aristidesz/apographe
|
|
6
|
+
Project-URL: Documentation, https://github.com/aristidesz/apographe/tree/main/docs
|
|
7
|
+
Project-URL: Changelog, https://github.com/aristidesz/apographe/blob/main/CHANGELOG.md
|
|
8
|
+
Project-URL: Issues, https://github.com/aristidesz/apographe/issues
|
|
9
|
+
Author-email: Aristides Zenonos <10169524+aristidesz@users.noreply.github.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: data-collection,etl,open-data,time-series,web-scraping
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.11
|
|
23
|
+
Requires-Dist: defusedxml>=0.7
|
|
24
|
+
Requires-Dist: requests>=2.31
|
|
25
|
+
Provides-Extra: cy-fuel
|
|
26
|
+
Requires-Dist: beautifulsoup4>=4.12; extra == 'cy-fuel'
|
|
27
|
+
Requires-Dist: pandas>=2.2; extra == 'cy-fuel'
|
|
28
|
+
Requires-Dist: pdfplumber>=0.11; extra == 'cy-fuel'
|
|
29
|
+
Requires-Dist: pyvirtualdisplay>=3.0; extra == 'cy-fuel'
|
|
30
|
+
Requires-Dist: selenium>=4.36; extra == 'cy-fuel'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# Apographe
|
|
34
|
+
|
|
35
|
+
[](https://pypi.org/project/apographe/)
|
|
36
|
+
[](https://pypi.org/project/apographe/)
|
|
37
|
+
[](https://github.com/aristidesz/apographe/actions/workflows/ci.yml)
|
|
38
|
+
[](https://github.com/aristidesz/apographe/blob/main/LICENSE)
|
|
39
|
+
|
|
40
|
+
**Collect public data, day by day, into plain CSV files.**
|
|
41
|
+
|
|
42
|
+
Governments and public bodies publish data on websites and APIs, usually as
|
|
43
|
+
today's snapshot with no history. Apographe collects it on a schedule and saves
|
|
44
|
+
it as dated CSV files you can open in pandas, Excel or anything else. Run it
|
|
45
|
+
every day and you build your own history.
|
|
46
|
+
|
|
47
|
+
Prices are the first sources (supermarket products, petrol stations), not the
|
|
48
|
+
limit: a source is one small class, so any public dataset can be added.
|
|
49
|
+
|
|
50
|
+
*ἀπογραφή (apographḗ): a register, the official record of what was counted.*
|
|
51
|
+
|
|
52
|
+
## Getting started
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install apographe
|
|
56
|
+
|
|
57
|
+
apographe list # what you can collect
|
|
58
|
+
apographe run cy.ekalathi --limit 5 --out data # try it: 5 products, about a minute
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Among the files it writes:
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
data/cy/ekalathi/
|
|
65
|
+
├── products.csv what each product is
|
|
66
|
+
├── stores.csv each store, its chain and location
|
|
67
|
+
└── store_prices/2026/2026-10-12.csv.gz the price of each product in each store
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Read it with pandas:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
import glob
|
|
74
|
+
import pandas as pd
|
|
75
|
+
|
|
76
|
+
files = glob.glob("data/cy/ekalathi/store_prices/*/*.csv.gz")
|
|
77
|
+
prices = pd.concat(map(pd.read_csv, files))
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Leave out `--limit 5` to collect every product (about 45 minutes, since
|
|
81
|
+
Apographe waits a few seconds between requests).
|
|
82
|
+
|
|
83
|
+
Or from Python:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from apographe import run
|
|
87
|
+
|
|
88
|
+
report = run("cy.ekalathi", out="data", limit=5)
|
|
89
|
+
print(report.rows) # rows written per table
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Some jobs need credentials; `apographe info <source>` lists them.
|
|
93
|
+
|
|
94
|
+
## Sources
|
|
95
|
+
|
|
96
|
+
| Source | What it collects | Install |
|
|
97
|
+
|---|---|---|
|
|
98
|
+
| `cy.ekalathi` | Cyprus supermarket prices: every product in every store, from [e-kalathi.gov.cy](https://www.e-kalathi.gov.cy) | `pip install apographe` |
|
|
99
|
+
| `cy.fuel` | Cyprus fuel prices at every petrol station. The default job scrapes the [Ministry's observatory](https://eforms.eservices.cyprus.gov.cy/MCIT/MCIT/PetroleumPrices) (needs Chrome, [caveats](https://github.com/aristidesz/apographe/blob/main/docs/sources/cy-fuel.md#caveats-of-the-default-prices-job)); the `api` job uses the official API (needs approval and credentials) | `pip install "apographe[cy-fuel]"` |
|
|
100
|
+
|
|
101
|
+
## Learn more
|
|
102
|
+
|
|
103
|
+
- [Commands and options](https://github.com/aristidesz/apographe/blob/main/docs/usage.md)
|
|
104
|
+
- [Getting started notebook](https://github.com/aristidesz/apographe/blob/main/notebooks/00_getting_started.ipynb)
|
|
105
|
+
- [Using it from Python](https://github.com/aristidesz/apographe/blob/main/docs/library.md)
|
|
106
|
+
- [Collecting every day](https://github.com/aristidesz/apographe/blob/main/docs/scheduling.md)
|
|
107
|
+
- [What each source collects](https://github.com/aristidesz/apographe/tree/main/docs/sources)
|
|
108
|
+
- [Adding a source for your country](https://github.com/aristidesz/apographe/blob/main/docs/adding-a-source.md)
|
|
109
|
+
- [Contributing](https://github.com/aristidesz/apographe/blob/main/CONTRIBUTING.md)
|
|
110
|
+
|
|
111
|
+
Apographe makes one request at a time, waits between requests and identifies
|
|
112
|
+
itself to the sites it reads. It is an independent project, not affiliated with
|
|
113
|
+
any of them; check each site's terms before you collect from it.
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
MIT. The data you collect belongs to its publishers.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Apographe
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/apographe/)
|
|
4
|
+
[](https://pypi.org/project/apographe/)
|
|
5
|
+
[](https://github.com/aristidesz/apographe/actions/workflows/ci.yml)
|
|
6
|
+
[](https://github.com/aristidesz/apographe/blob/main/LICENSE)
|
|
7
|
+
|
|
8
|
+
**Collect public data, day by day, into plain CSV files.**
|
|
9
|
+
|
|
10
|
+
Governments and public bodies publish data on websites and APIs, usually as
|
|
11
|
+
today's snapshot with no history. Apographe collects it on a schedule and saves
|
|
12
|
+
it as dated CSV files you can open in pandas, Excel or anything else. Run it
|
|
13
|
+
every day and you build your own history.
|
|
14
|
+
|
|
15
|
+
Prices are the first sources (supermarket products, petrol stations), not the
|
|
16
|
+
limit: a source is one small class, so any public dataset can be added.
|
|
17
|
+
|
|
18
|
+
*ἀπογραφή (apographḗ): a register, the official record of what was counted.*
|
|
19
|
+
|
|
20
|
+
## Getting started
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install apographe
|
|
24
|
+
|
|
25
|
+
apographe list # what you can collect
|
|
26
|
+
apographe run cy.ekalathi --limit 5 --out data # try it: 5 products, about a minute
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Among the files it writes:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
data/cy/ekalathi/
|
|
33
|
+
├── products.csv what each product is
|
|
34
|
+
├── stores.csv each store, its chain and location
|
|
35
|
+
└── store_prices/2026/2026-10-12.csv.gz the price of each product in each store
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Read it with pandas:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
import glob
|
|
42
|
+
import pandas as pd
|
|
43
|
+
|
|
44
|
+
files = glob.glob("data/cy/ekalathi/store_prices/*/*.csv.gz")
|
|
45
|
+
prices = pd.concat(map(pd.read_csv, files))
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Leave out `--limit 5` to collect every product (about 45 minutes, since
|
|
49
|
+
Apographe waits a few seconds between requests).
|
|
50
|
+
|
|
51
|
+
Or from Python:
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from apographe import run
|
|
55
|
+
|
|
56
|
+
report = run("cy.ekalathi", out="data", limit=5)
|
|
57
|
+
print(report.rows) # rows written per table
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Some jobs need credentials; `apographe info <source>` lists them.
|
|
61
|
+
|
|
62
|
+
## Sources
|
|
63
|
+
|
|
64
|
+
| Source | What it collects | Install |
|
|
65
|
+
|---|---|---|
|
|
66
|
+
| `cy.ekalathi` | Cyprus supermarket prices: every product in every store, from [e-kalathi.gov.cy](https://www.e-kalathi.gov.cy) | `pip install apographe` |
|
|
67
|
+
| `cy.fuel` | Cyprus fuel prices at every petrol station. The default job scrapes the [Ministry's observatory](https://eforms.eservices.cyprus.gov.cy/MCIT/MCIT/PetroleumPrices) (needs Chrome, [caveats](https://github.com/aristidesz/apographe/blob/main/docs/sources/cy-fuel.md#caveats-of-the-default-prices-job)); the `api` job uses the official API (needs approval and credentials) | `pip install "apographe[cy-fuel]"` |
|
|
68
|
+
|
|
69
|
+
## Learn more
|
|
70
|
+
|
|
71
|
+
- [Commands and options](https://github.com/aristidesz/apographe/blob/main/docs/usage.md)
|
|
72
|
+
- [Getting started notebook](https://github.com/aristidesz/apographe/blob/main/notebooks/00_getting_started.ipynb)
|
|
73
|
+
- [Using it from Python](https://github.com/aristidesz/apographe/blob/main/docs/library.md)
|
|
74
|
+
- [Collecting every day](https://github.com/aristidesz/apographe/blob/main/docs/scheduling.md)
|
|
75
|
+
- [What each source collects](https://github.com/aristidesz/apographe/tree/main/docs/sources)
|
|
76
|
+
- [Adding a source for your country](https://github.com/aristidesz/apographe/blob/main/docs/adding-a-source.md)
|
|
77
|
+
- [Contributing](https://github.com/aristidesz/apographe/blob/main/CONTRIBUTING.md)
|
|
78
|
+
|
|
79
|
+
Apographe makes one request at a time, waits between requests and identifies
|
|
80
|
+
itself to the sites it reads. It is an independent project, not affiliated with
|
|
81
|
+
any of them; check each site's terms before you collect from it.
|
|
82
|
+
|
|
83
|
+
## License
|
|
84
|
+
|
|
85
|
+
MIT. The data you collect belongs to its publishers.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Adding a source
|
|
2
|
+
|
|
3
|
+
A source is one class. It names its jobs, and each job writes tables through the
|
|
4
|
+
`Store` it is given. The framework does the rest: the command line, the day the
|
|
5
|
+
run belongs to, the output folder, the report.
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
from apographe.core.http import PoliteClient
|
|
9
|
+
from apographe.core.source import Options, Report, Source
|
|
10
|
+
from apographe.core.storage import Store
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ExampleFuelSource(Source):
|
|
14
|
+
id = "xx.fuel" # country code, then a short name
|
|
15
|
+
title = "Example fuel prices"
|
|
16
|
+
country = "XX" # ISO 3166-1 alpha-2
|
|
17
|
+
homepage = "https://fuel.example.gov"
|
|
18
|
+
timezone = "Europe/Athens" # "today" is taken here
|
|
19
|
+
jobs = {"prices": "every station's prices, now"}
|
|
20
|
+
default_jobs = ("prices",)
|
|
21
|
+
|
|
22
|
+
def run(self, job: str, out: Store, options: Options) -> Report:
|
|
23
|
+
http = PoliteClient(
|
|
24
|
+
"https://fuel.example.gov/api", min_interval=options.delay or 3
|
|
25
|
+
)
|
|
26
|
+
stations = http.get_json("/stations")
|
|
27
|
+
day = options.day.isoformat()
|
|
28
|
+
rows = [
|
|
29
|
+
{"date": day, "station_id": s["id"], "fuel": s["fuel"], "price": s["price"]}
|
|
30
|
+
for s in stations
|
|
31
|
+
]
|
|
32
|
+
report = Report()
|
|
33
|
+
path, count = out.write_csv(
|
|
34
|
+
f"prices/{day[:4]}/{day}.csv.gz",
|
|
35
|
+
rows,
|
|
36
|
+
["date", "station_id", "fuel", "price"],
|
|
37
|
+
)
|
|
38
|
+
report.add("prices", count, path)
|
|
39
|
+
return report
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Shipping it
|
|
43
|
+
|
|
44
|
+
Put the module under `src/apographe/sources/<country>/`, add it to `BUILT_IN`
|
|
45
|
+
in `src/apographe/core/registry.py`, add tests with a few trimmed real responses
|
|
46
|
+
under `tests/fixtures/`, and open a pull request.
|
|
47
|
+
|
|
48
|
+
## Conventions
|
|
49
|
+
|
|
50
|
+
- **One file per day** for anything collected daily: `<table>/YYYY/YYYY-MM-DD.csv.gz`.
|
|
51
|
+
Lookups that change slowly (stores, chains) are one CSV, rewritten.
|
|
52
|
+
- **Ids in every row** so tables join: `product_id`, `store_id`, `chain_id`.
|
|
53
|
+
- **Keep the raw response** when it is small, under `raw/`, so a parsing fix can
|
|
54
|
+
rebuild the tables.
|
|
55
|
+
- **Be polite:** one request at a time, a few seconds apart; never parallelise
|
|
56
|
+
requests to a public site.
|
|
57
|
+
- **Raise `LoginWall`** (the `PoliteClient` does this for you) when a site starts
|
|
58
|
+
asking for a login, so scheduled runs end cleanly instead of failing.
|
|
59
|
+
- **Print counts, never data:** logs of scheduled runs are often public.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Using Apographe from Python
|
|
2
|
+
|
|
3
|
+
Everything the command line does is one function, `run`:
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
from apographe import run
|
|
7
|
+
|
|
8
|
+
report = run("cy.ekalathi", out="data", limit=5)
|
|
9
|
+
print(report.rows) # {"products": 5, ...}: rows written per table
|
|
10
|
+
print(report.files) # the files written
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`run` takes the same options as `apographe run`: `jobs`, `day` (a date or
|
|
14
|
+
`"2026-10-01"`), `limit`, `only`, `delay`, `inputs` and `force`. `help(run)`
|
|
15
|
+
lists them all. It raises `ValueError` for a job the source does not have,
|
|
16
|
+
`LoginWall` when the site now asks for a login, and `SourceError` for anything
|
|
17
|
+
else that goes wrong. It prints nothing.
|
|
18
|
+
|
|
19
|
+
## Your own source
|
|
20
|
+
|
|
21
|
+
Pass a `Source` (an instance or the class) instead of an id:
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from apographe import Report, Source, run
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Stations(Source):
|
|
28
|
+
id = "xx.fuel"
|
|
29
|
+
title = "Example fuel prices"
|
|
30
|
+
country = "XX"
|
|
31
|
+
homepage = "https://fuel.example.gov"
|
|
32
|
+
jobs = {"prices": "every station's prices, now"}
|
|
33
|
+
default_jobs = ("prices",)
|
|
34
|
+
|
|
35
|
+
def run(self, job, out, options):
|
|
36
|
+
rows = [{"station": "A", "price": 1.5}] # fetch your data here
|
|
37
|
+
path, count = out.write_csv(
|
|
38
|
+
f"prices/{options.day}.csv", rows, ["station", "price"]
|
|
39
|
+
)
|
|
40
|
+
report = Report()
|
|
41
|
+
report.add("prices", count, path)
|
|
42
|
+
return report
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
run(Stations(), out="data")
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
[Adding a source](adding-a-source.md) shows a full one and the conventions.
|
|
49
|
+
|
|
50
|
+
## What you can import
|
|
51
|
+
|
|
52
|
+
| Name | What it is |
|
|
53
|
+
|---|---|
|
|
54
|
+
| `run` | Run a source |
|
|
55
|
+
| `Source`, `Options`, `Report` | Write a source: the class, its run settings, what it hands back |
|
|
56
|
+
| `Store` | The folder a source writes into: `write_csv`, `write_json`, `read_csv` |
|
|
57
|
+
| `PoliteClient` | A JSON client that waits between requests and retries |
|
|
58
|
+
| `ClientCredentials` | OAuth sign-in for sources behind an API gateway |
|
|
59
|
+
| `SourceError`, `LoginWall` | The errors above |
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Collecting every day
|
|
2
|
+
|
|
3
|
+
Public data only becomes a history if you collect them every day. Apographe is a
|
|
4
|
+
command, so any scheduler works: cron on a server, a container on a timer, or
|
|
5
|
+
GitHub Actions.
|
|
6
|
+
|
|
7
|
+
## GitHub Actions
|
|
8
|
+
|
|
9
|
+
This workflow collects e-kalathi every morning and commits the new files to the
|
|
10
|
+
repository it runs in. Keep that repository private if the data should be.
|
|
11
|
+
|
|
12
|
+
```yaml
|
|
13
|
+
name: Collect data
|
|
14
|
+
|
|
15
|
+
on:
|
|
16
|
+
schedule:
|
|
17
|
+
- cron: "30 4 * * *" # 07:30 in Cyprus in summer
|
|
18
|
+
workflow_dispatch:
|
|
19
|
+
|
|
20
|
+
permissions:
|
|
21
|
+
contents: write
|
|
22
|
+
|
|
23
|
+
jobs:
|
|
24
|
+
collect:
|
|
25
|
+
runs-on: ubuntu-latest
|
|
26
|
+
timeout-minutes: 120
|
|
27
|
+
steps:
|
|
28
|
+
- uses: actions/checkout@v4
|
|
29
|
+
- uses: actions/setup-python@v5
|
|
30
|
+
with:
|
|
31
|
+
python-version: "3.12"
|
|
32
|
+
- run: pip install apographe
|
|
33
|
+
- run: apographe run cy.ekalathi --out .
|
|
34
|
+
- run: |
|
|
35
|
+
git config user.name "github-actions[bot]"
|
|
36
|
+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
37
|
+
git add cy
|
|
38
|
+
git commit -m "e-kalathi $(date -u +%F)" || exit 0
|
|
39
|
+
git push
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Pin the version you use (`pip install apographe==0.1.0`) so an upgrade never
|
|
43
|
+
surprises a scheduled run.
|
|
44
|
+
|
|
45
|
+
## cron
|
|
46
|
+
|
|
47
|
+
```cron
|
|
48
|
+
30 7 * * * cd /path/to/data && apographe run cy.ekalathi --out . >> collect.log 2>&1
|
|
49
|
+
```
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# cy.ekalathi: Cyprus supermarket prices
|
|
2
|
+
|
|
3
|
+
[e-kalathi.gov.cy](https://www.e-kalathi.gov.cy) is the Cypriot government's
|
|
4
|
+
supermarket price comparison site. It lists about 476 everyday products and their
|
|
5
|
+
price in each of more than 140 stores, listed under 48 chains.
|
|
6
|
+
|
|
7
|
+
## Jobs
|
|
8
|
+
|
|
9
|
+
| Job | Requests | Writes |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| `lookups` | 3 | `chains.csv`, `regions.csv`, `categories.csv` |
|
|
12
|
+
| `catalogue` | ~5 | `catalogue/YYYY/DATE.csv.gz` (cheapest price per product), `products.csv`, the raw response |
|
|
13
|
+
| `store-prices` | 1 per product (~476) | `store_prices/YYYY/DATE.csv.gz`, `stores.csv` |
|
|
14
|
+
| `history` | 1 per product | `history/<product_id>.csv`: daily cheapest price since 3 Sep 2025 |
|
|
15
|
+
|
|
16
|
+
`apographe run cy.ekalathi` runs the first three. A full day takes about 45
|
|
17
|
+
minutes at the default 3 seconds between requests. If it stops part way, the
|
|
18
|
+
next run on the same day carries on where it left off.
|
|
19
|
+
|
|
20
|
+
## Tables
|
|
21
|
+
|
|
22
|
+
| Table | Key | Columns |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| products | product_id | code, name, category, category_en, category_id |
|
|
25
|
+
| catalogue | date, product_id | price (cheapest in Cyprus), previous_price, store_count |
|
|
26
|
+
| store_prices | date, product_id, store_id | price, price_before_offer, on_offer |
|
|
27
|
+
| stores | store_id | name, chain_id, chain, phone, address, postcode, region_id, lat, lon, last_seen |
|
|
28
|
+
| chains | chain_id | name |
|
|
29
|
+
| regions | region_id | name |
|
|
30
|
+
| categories | category_id | code, name, name_en, parent_id |
|
|
31
|
+
| history | date, product_id | price |
|
|
32
|
+
|
|
33
|
+
Example, Kykkos water 500 ml on 9 Oct 2026:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
catalogue: 2026-10-09, 2074, 0.25, 0.23, 115
|
|
37
|
+
store_prices: 2026-10-09, 2074, 539, 0.25, 0.25, False
|
|
38
|
+
stores: 539, ALPHA SIGMA SUPERMARKET Περιοχή Φασούρι, 452, ALPHA SIGMA, …, 4640, 3, 34.644167, 32.944778
|
|
39
|
+
chains: 452, ALPHA SIGMA
|
|
40
|
+
regions: 3, ΛΕΜΕΣΟΣ
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Things to know
|
|
44
|
+
|
|
45
|
+
- **Ids are per table.** Chain 539 (ΣΥΠΑΛ - ΛΙΜΝΙΑ) and store 539 (ALPHA SIGMA
|
|
46
|
+
Φασούρι) are unrelated.
|
|
47
|
+
- **Some "chains" are single shops,** such as each SMART DISCOUNT SHOP branch.
|
|
48
|
+
- **`store_count` counts stores, not chains,** although the site calls it
|
|
49
|
+
`numberOfChains`.
|
|
50
|
+
- **Region comes from the postcode** (1xxx-2xxx Nicosia, 3xxx-4xxx Limassol,
|
|
51
|
+
5xxx Famagusta, 6xxx-7xxx Larnaca, 8xxx Paphos), or from the town named in the
|
|
52
|
+
address when there is no postcode; store rows carry no region of their own.
|
|
53
|
+
- **Past prices exist only as the cheapest per product.** Per-store prices can
|
|
54
|
+
be collected from the day you start.
|
|
55
|
+
|
|
56
|
+
## The site's API
|
|
57
|
+
|
|
58
|
+
The site's pages call a JSON backend at
|
|
59
|
+
`https://www.e-kalathi.gov.cy/ekalathi-website-server/api`. It is not a
|
|
60
|
+
published API and may change without notice. None of these calls needs a login.
|
|
61
|
+
|
|
62
|
+
| Path | Arguments | Returns | Time |
|
|
63
|
+
|---|---|---|---|
|
|
64
|
+
| `/fetch-product-list` | `page`, `size`, `sort=DEFAULT,ASC`; optional `categoryIds`, `productName` | Paged products: `productMasterId`, `code`, `name`, `startPrice` (cheapest today), `previousPrice`, `productCategoryName(English)`, `numberOfChains` | ~1 s per page |
|
|
65
|
+
| `/retail/fetch-retail-branch-list` | `productId`, `page`, `size`, `sort` | Paged store rows: `id`, `name`, `companyName`, `landPhone`, `postalAddress`, `branchLatitude/Longitude` (degrees-minutes-seconds), `retailerProductPrice`, `retailerInitialProductPrice`, `isInOfferOrDiscount`, and the chain logo as base64 (`companyPhotoFile`, ~100 KB per row) | ~4 s, ~11 MB |
|
|
66
|
+
| `/fetch-product-price-diagram` | `id`, `from`, `to` (YYYY-MM-DD); optional `cId` (chain), `rId` (region) | `[{date, price}]`, the cheapest in Cyprus each day, or one chain's/region's | ~30 s; often times out, so retried |
|
|
67
|
+
| `/fetch-product?id=` | product id | Product details, no prices | ~1 s |
|
|
68
|
+
| `/fetch-companies`, `/fetch-regions`, `/fetch-product-categories` | none | Lookup lists | ~1 s |
|
|
69
|
+
|
|
70
|
+
Paged calls answer `{"content": [...], "last": bool, "totalPages": n, ...}`.
|
|
71
|
+
`/fetch-retail-branches`, `/fetch-basket-types` and `/fetch-company-offers` need
|
|
72
|
+
a login (HTTP 401).
|