glfm 1.0.4__py3-none-any.whl

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.
Files changed (55) hide show
  1. glfm/__init__.py +19 -0
  2. glfm/_version.py +18 -0
  3. glfm/build_unified.py +197 -0
  4. glfm/config.py +170 -0
  5. glfm/data/override_scripts.json +23 -0
  6. glfm/data/uralic_languages.json +8 -0
  7. glfm/download.py +127 -0
  8. glfm/loaders/__init__.py +18 -0
  9. glfm/loaders/load_cldr_likely_subtags.py +167 -0
  10. glfm/loaders/load_glottolog.py +79 -0
  11. glfm/loaders/load_iso_639.py +187 -0
  12. glfm/loaders/load_override_scripts.py +51 -0
  13. glfm/loaders/load_pos_stats.py +54 -0
  14. glfm/loaders/load_uralic_languages.py +70 -0
  15. glfm/loaders/load_wiktionary_languages.py +61 -0
  16. glfm/loaders/load_written_languages.py +66 -0
  17. glfm/logic/__init__.py +17 -0
  18. glfm/logic/build_bcp47.py +42 -0
  19. glfm/logic/build_extinct_json.py +97 -0
  20. glfm/logic/decide_default_region.py +46 -0
  21. glfm/logic/decide_default_script.py +89 -0
  22. glfm/logic/decide_fallback.py +37 -0
  23. glfm/logic/is_uralic.py +41 -0
  24. glfm/logic/resolve_cldr_key.py +35 -0
  25. glfm/logic/set_uralic_langs.py +34 -0
  26. glfm/models/__init__.py +11 -0
  27. glfm/models/iso_639_2.py +108 -0
  28. glfm/models/iso_639_3.py +140 -0
  29. glfm/models/language.py +72 -0
  30. glfm/paths.py +70 -0
  31. glfm/run_full_pipeline.py +215 -0
  32. glfm/tools/__init__.py +9 -0
  33. glfm/tools/all_language_tests.py +169 -0
  34. glfm/tools/build_consumer_dataset.py +169 -0
  35. glfm/tools/build_glottolog_json.py +302 -0
  36. glfm/tools/build_iso_data.py +213 -0
  37. glfm/tools/build_pos_stats.py +111 -0
  38. glfm/tools/compress_stdlib.py +125 -0
  39. glfm/tools/compute_nearest_languages.py +336 -0
  40. glfm/tools/convert_to_msgpack.py +112 -0
  41. glfm/tools/generate_iso_files.py +179 -0
  42. glfm/tools/generate_report.py +216 -0
  43. glfm/tools/parse_iso_files.py +249 -0
  44. glfm/tools/run_full_pipeline_test.py +129 -0
  45. glfm/tools/validate_bcp47.py +87 -0
  46. glfm/tools/validate_fallbacks.py +106 -0
  47. glfm/tools/validate_glottolog.py +109 -0
  48. glfm/tools/validate_iso_consistency.py +101 -0
  49. glfm/tools/validate_pos_stats.py +88 -0
  50. glfm/tools/visualize_fallbacks.py +143 -0
  51. glfm-1.0.4.dist-info/METADATA +344 -0
  52. glfm-1.0.4.dist-info/RECORD +55 -0
  53. glfm-1.0.4.dist-info/WHEEL +5 -0
  54. glfm-1.0.4.dist-info/licenses/LICENCE +21 -0
  55. glfm-1.0.4.dist-info/top_level.txt +1 -0
glfm/__init__.py ADDED
@@ -0,0 +1,19 @@
1
+ # __init__.py
2
+ """
3
+ GLFM – Global Language Family Mapper
4
+
5
+ Kielimetatietokanta ja -työkalut 7900+ kielelle (ISO 639-3).
6
+
7
+ Yksi totuuden lähde versiolle: _version.py
8
+ """
9
+
10
+ try:
11
+ from _version import __version__, __version_tuple__
12
+ except ImportError:
13
+ try:
14
+ from glfm._version import __version__, __version_tuple__
15
+ except ImportError:
16
+ __version__ = "1.0.4"
17
+ __version_tuple__ = (1, 0, 4)
18
+
19
+ __all__ = ["__version__", "__version_tuple__"]
glfm/_version.py ADDED
@@ -0,0 +1,18 @@
1
+ # glfm/_version.py
2
+ """
3
+ Yksi totuuden lähde versiolle.
4
+
5
+ Käyttö:
6
+ from glfm._version import __version__
7
+ from glfm._version import __version_tuple__
8
+
9
+ Versio noudattaa PEP 440 -standardia:
10
+ MAJOR.MINOR.PATCH[.devN | .aN | .bN | .rcN]
11
+ """
12
+
13
+ __version__ = "1.0.4"
14
+ __version_tuple__ = tuple(int(x) for x in __version__.split(".")[:3])
15
+
16
+ # Valinnainen: git-commit, build-päivä (voi päivittää CI:ssä)
17
+ __build_date__ = "2026-10-26"
18
+ __git_commit__ = "unknown"
glfm/build_unified.py ADDED
@@ -0,0 +1,197 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ #
4
+ # GLFM Project
5
+ # Copyright (c) 2026 Tuomas Lähteenmäki
6
+ # Version 1.0.4
7
+ #
8
+ # https://codeberg.org/lahtis/GLFM
9
+ # Licensed under the MIT License.
10
+ #
11
+
12
+ import gzip
13
+ import json
14
+ import logging
15
+ from pathlib import Path
16
+ from typing import Any, Dict
17
+
18
+ from glfm.loaders.load_iso_639 import load_iso_639
19
+ from glfm.loaders.load_cldr_likely_subtags import load_cldr_likely_subtags
20
+ from glfm.loaders.load_wiktionary_languages import load_wiktionary_languages
21
+ from glfm.loaders.load_uralic_languages import load_uralic_languages
22
+ from glfm.loaders.load_written_languages import load_written_languages
23
+ from glfm.loaders.load_glottolog import load_glottolog
24
+ from glfm.loaders.load_pos_stats import load_pos_stats
25
+ from glfm.loaders.load_override_scripts import load_override_scripts
26
+
27
+ from glfm.logic.decide_default_script import decide_default_script
28
+ from glfm.logic.decide_default_region import decide_default_region
29
+ from glfm.logic.build_bcp47 import build_bcp47
30
+ from glfm.logic.decide_fallback import decide_fallback
31
+ from glfm.logic.is_uralic import is_uralic
32
+ from glfm.logic.set_uralic_langs import set_uralic_langs
33
+
34
+ from glfm.models.language import Language
35
+ from glfm.paths import get_output_dir
36
+
37
+ log = logging.getLogger(__name__)
38
+
39
+ # ---------------------------------------------------------
40
+ # ETÄISYYS- JA LÄHDEMETATIEDOT (URIEL / lang2vec)
41
+ # ---------------------------------------------------------
42
+ DISTANCE_SOURCE = "lang2vec / URIEL"
43
+ DISTANCE_TYPE = "fam"
44
+ DISTANCE_BASES = ["WALS", "SSWL", "PHOIBLE", "Ethnologue", "Glottolog"]
45
+
46
+ # ---------------------------------------------------------
47
+ # OUTPUT-kansiot (glfm.paths:stä)
48
+ # ---------------------------------------------------------
49
+
50
+ OUTPUT_ROOT = get_output_dir()
51
+
52
+ SUBFOLDERS = ["unified", "validation", "dialects", "final", "logs"]
53
+
54
+ OUTPUT = OUTPUT_ROOT / "unified" / "unified_languages.json"
55
+ OUTPUT_GZ = OUTPUT_ROOT / "unified" / "unified_languages.json.gz"
56
+
57
+
58
+ def _ensure_dirs() -> None:
59
+ """Luo output-kansiot. Kutsutaan main():sta, ei importin aikana."""
60
+ for folder in SUBFOLDERS:
61
+ (OUTPUT_ROOT / folder).mkdir(parents=True, exist_ok=True)
62
+
63
+
64
+ # ---------------------------------------------------------
65
+ # Unified-rakenteen rakentaminen
66
+ # ---------------------------------------------------------
67
+
68
+ def build_unified() -> Dict[str, Any]:
69
+
70
+ # --- Lataa datalähteet ---
71
+ iso = load_iso_639()
72
+ cldr = load_cldr_likely_subtags()
73
+ wikt = load_wiktionary_languages()
74
+ written = load_written_languages()
75
+ uralic_set = load_uralic_languages()
76
+ glottolog = load_glottolog()
77
+ pos_stats = load_pos_stats()
78
+ overrides = load_override_scripts()
79
+
80
+ if not uralic_set:
81
+ log.warning("uralic_languages.json puuttuu tai on tyhjä – "
82
+ "kaikki uralicNLP-flagit ovat False")
83
+
84
+ set_uralic_langs(uralic_set)
85
+
86
+ unified: Dict[str, Any] = {}
87
+
88
+ # --- Käy läpi kaikki ISO-kielet ---
89
+ for lang_id, iso_info in iso.items():
90
+
91
+ w = wikt.get(lang_id, {})
92
+ w_written = written.get(lang_id, {})
93
+ glotto = glottolog.get(lang_id, {})
94
+ pos = pos_stats.get(lang_id, {})
95
+
96
+ # Nimet
97
+ name = w.get("name") or iso_info.get("name") or lang_id
98
+ official_name = w.get("official_name") or name
99
+
100
+ # ISO-koodit
101
+ iso1 = iso_info.get("iso639_1", "")
102
+ iso2B = iso_info.get("iso639_2B", "")
103
+ iso2T = iso_info.get("iso639_2T", "")
104
+ iso3 = iso_info.get("iso639_3", "")
105
+ iso5 = iso_info.get("iso639_5", "")
106
+ language_type = iso_info.get("language_type", "")
107
+
108
+ # --- Skripti ja alue dynaamisesti lähteistä ---
109
+ script = decide_default_script(lang_id, w, cldr, w_written, iso_info, overrides)
110
+ region = decide_default_region(lang_id, w, cldr, iso_info)
111
+
112
+ # --- Kirjoitetun kielen tila määräytyy YKSINOMAAN todennetusta skriptistä ---
113
+ is_written_flag = bool(script and script.upper() != "UNKNOWN")
114
+ scripts = [script] if is_written_flag else []
115
+
116
+ # --- BCP-47 ---
117
+ bcp47 = build_bcp47(lang_id, script if is_written_flag else "", region, iso_info)
118
+
119
+ # --- Fallback ja Uralic-tuki ---
120
+ fallback = decide_fallback(lang_id, w)
121
+ uralic = is_uralic(lang_id)
122
+
123
+ # --- Glottolog-kentät ---
124
+ glottocode = (glotto.get("glottocode")
125
+ or w_written.get("glottocode", ""))
126
+ family = (glotto.get("family")
127
+ or w_written.get("family", ""))
128
+
129
+ # Luo Language-olio
130
+ lang_obj = Language(
131
+ id=lang_id,
132
+ name=name,
133
+ official_name=official_name,
134
+ iso639_1=iso1,
135
+ iso639_2B=iso2B,
136
+ iso639_2T=iso2T,
137
+ iso639_3=iso3,
138
+ iso639_5=iso5,
139
+ language_type=language_type,
140
+ default_script=script if is_written_flag else "",
141
+ default_region=region,
142
+ bcp47=bcp47,
143
+ fallback=fallback,
144
+ uralicNLP=uralic,
145
+ written=is_written_flag,
146
+ written_scripts=scripts,
147
+ glottocode=glottocode,
148
+ family=family,
149
+ glottolog=glotto,
150
+ pos_stats=pos,
151
+ )
152
+
153
+ lang_dict = lang_obj.__dict__
154
+
155
+ # --- Etäisyysmetatiedot: AINA läsnä ---
156
+ lang_dict["distance_source"] = DISTANCE_SOURCE
157
+ lang_dict["distance_type"] = DISTANCE_TYPE
158
+ lang_dict["distance_bases"] = DISTANCE_BASES
159
+ lang_dict["nearest_languages"] = []
160
+
161
+ unified[lang_id] = lang_dict
162
+
163
+ return unified
164
+
165
+
166
+ # ---------------------------------------------------------
167
+ # Tallennus
168
+ # ---------------------------------------------------------
169
+
170
+ def _save(unified: Dict[str, Any]) -> None:
171
+ """Tallentaa sekä pretty-JSON (debug) että gzip-JSON (tuotanto)."""
172
+ # Pretty (debug, gitignoressa)
173
+ with open(OUTPUT, "w", encoding="utf-8") as f:
174
+ json.dump(unified, f, ensure_ascii=False, indent=2)
175
+ log.info("Kirjoitettu: %s (%.1f MB)",
176
+ OUTPUT, OUTPUT.stat().st_size / 1e6)
177
+
178
+ # Gzip (tuotanto, pieni)
179
+ with gzip.open(OUTPUT_GZ, "wt", encoding="utf-8") as f:
180
+ json.dump(unified, f, ensure_ascii=False, separators=(",", ":"))
181
+ log.info("Kirjoitettu: %s (%.1f MB)",
182
+ OUTPUT_GZ, OUTPUT_GZ.stat().st_size / 1e6)
183
+
184
+
185
+ def main() -> None:
186
+ logging.basicConfig(
187
+ level=logging.INFO,
188
+ format="%(levelname)s %(name)s: %(message)s",
189
+ )
190
+ _ensure_dirs()
191
+ unified = build_unified()
192
+ _save(unified)
193
+ log.info("Valmis: %d kieltä", len(unified))
194
+
195
+
196
+ if __name__ == "__main__":
197
+ main()
glfm/config.py ADDED
@@ -0,0 +1,170 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ #
4
+ # GLFM Project
5
+ # Copyright (c) 2026 Tuomas Lähteenmäki
6
+ # Version 1.0.4
7
+ #
8
+ # https://codeberg.org/lahtis/GLFM
9
+ # Licensed under the MIT License.
10
+ #
11
+ # Konfiguraation lataus: config.toml + ympäristömuuttujat.
12
+ #
13
+
14
+ import copy
15
+ import logging
16
+ import os
17
+ from pathlib import Path
18
+
19
+ from glfm.paths import get_data_dir, get_output_dir
20
+
21
+ log = logging.getLogger(__name__)
22
+
23
+ # Oletuskonfiguraatio – ylikirjoitetaan config.toml:llä ja env-muuttujilla
24
+ DEFAULT_CONFIG = {
25
+ "sources": {
26
+ "iso_639_3": {
27
+ "url": (
28
+ "https://iso639-3.sil.org/sites/iso639-3/files/"
29
+ "downloads/iso-639-3.tab"
30
+ ),
31
+ },
32
+ "iso_639_3_names": {
33
+ "url": (
34
+ "https://iso639-3.sil.org/sites/iso639-3/files/"
35
+ "downloads/iso-639-3_Name_Index.tab"
36
+ ),
37
+ },
38
+ "iso_639_3_macrolanguages": {
39
+ "url": (
40
+ "https://iso639-3.sil.org/sites/iso639-3/files/"
41
+ "downloads/iso-639-3-macrolanguages.tab"
42
+ ),
43
+ },
44
+ "iso_639_5": {
45
+ "url": "https://id.loc.gov/vocabulary/iso639-5.skos.rdf",
46
+ },
47
+ "cldr": {
48
+ "version": "48.0.0",
49
+ "url": (
50
+ "https://raw.githubusercontent.com/unicode-org/"
51
+ "cldr-json/48.0.0/cldr-json/cldr-core/supplemental/"
52
+ "likelySubtags.json"
53
+ ),
54
+ },
55
+ "wikidata": {
56
+ "endpoint": "https://query.wikidata.org/sparql",
57
+ },
58
+ },
59
+ "download": {
60
+ "url": (
61
+ "https://codeberg.org/lahtis/GLFM/releases/download/"
62
+ "v1.0.4/data.zip"
63
+ ),
64
+ "sha256": "",
65
+ "timeout": 300,
66
+ },
67
+ "http": {
68
+ "user_agent": "glfm/1.0.4 (https://codeberg.org/lahtis/GLFM)",
69
+ "timeout": 60,
70
+ },
71
+ "paths": {
72
+ "cache_dir": "",
73
+ "data_dir": "",
74
+ "cache_dir_name": "glfm",
75
+ "data_subdir": "data",
76
+ },
77
+ "logging": {
78
+ "level": "INFO",
79
+ },
80
+ }
81
+
82
+ # Ympäristömuuttujat → config-avaimet
83
+ ENV_MAP = {
84
+ "GLFM_ISO_639_3_URL": ("sources", "iso_639_3", "url"),
85
+ "GLFM_ISO_639_3_NAMES_URL": ("sources", "iso_639_3_names", "url"),
86
+ "GLFM_ISO_639_3_MACRO_URL": ("sources", "iso_639_3_macrolanguages", "url"),
87
+ "GLFM_ISO_639_5_URL": ("sources", "iso_639_5", "url"),
88
+ "GLFM_CLDR_VERSION": ("sources", "cldr", "version"),
89
+ "GLFM_CLDR_URL": ("sources", "cldr", "url"),
90
+ "GLFM_WIKIDATA_ENDPOINT": ("sources", "wikidata", "endpoint"),
91
+ "GLFM_DATA_URL": ("download", "url"),
92
+ "GLFM_DATA_SHA256": ("download", "sha256"),
93
+ "GLFM_DATA_TIMEOUT": ("download", "timeout"),
94
+ "GLFM_USER_AGENT": ("http", "user_agent"),
95
+ "GLFM_HTTP_TIMEOUT": ("http", "timeout"),
96
+ "GLFM_CACHE_DIR": ("paths", "cache_dir"),
97
+ "GLFM_DATA_DIR": ("paths", "data_dir"),
98
+ "GLFM_LOG_LEVEL": ("logging", "level"),
99
+ }
100
+
101
+
102
+ def load_config() -> dict:
103
+ """
104
+ Lataa konfiguraation seuraavassa järjestyksessä:
105
+
106
+ 1. DEFAULT_CONFIG (kovakoodatut oletukset)
107
+ 2. config.toml (repon juuressa tai ~/.config/glfm/config.toml)
108
+ 3. Ympäristömuuttujat (GLFM_*)
109
+
110
+ Palauttaa syvän kopion – ei jaa tilaa DEFAULT_CONFIG:n kanssa.
111
+ """
112
+ config = copy.deepcopy(DEFAULT_CONFIG)
113
+
114
+ # 1. config.toml
115
+ config_path = get_config_path()
116
+ if config_path.exists():
117
+ try:
118
+ import tomllib
119
+ with open(config_path, "rb") as f:
120
+ user_config = tomllib.load(f)
121
+ _merge(config, user_config)
122
+ log.info("Luettu config: %s", config_path)
123
+ except Exception as e:
124
+ log.warning("Config-virhe (%s): %s", config_path, e)
125
+ else:
126
+ log.debug("config.toml puuttuu (%s), käytetään oletuksia",
127
+ config_path)
128
+
129
+ # 2. Ympäristömuuttujat
130
+ for env_key, path in ENV_MAP.items():
131
+ if value := os.environ.get(env_key):
132
+ _set_nested(config, path, value)
133
+ log.debug("Env: %s → %s", env_key, ".".join(path))
134
+
135
+ return config
136
+
137
+
138
+ def get_config_path() -> Path:
139
+ """Palauttaa config.toml-polkua."""
140
+ if env := os.environ.get("GLFM_CONFIG"):
141
+ return Path(env).expanduser().resolve()
142
+
143
+ xdg = os.environ.get("XDG_CONFIG_HOME")
144
+ if xdg:
145
+ return Path(xdg) / "glfm" / "config.toml"
146
+
147
+ return Path.home() / ".config" / "glfm" / "config.toml"
148
+
149
+
150
+ def _set_nested(config: dict, path: tuple, value) -> None:
151
+ """Asettaa arvon syvään dictiin."""
152
+ current = config
153
+ for key in path[:-1]:
154
+ if key not in current or not isinstance(current[key], dict):
155
+ current[key] = {}
156
+ current = current[key]
157
+ current[path[-1]] = value
158
+
159
+
160
+ def _merge(base: dict, override: dict) -> None:
161
+ """Syvä merge: override korvaa base:n arvot."""
162
+ for key, value in override.items():
163
+ if (
164
+ key in base
165
+ and isinstance(base[key], dict)
166
+ and isinstance(value, dict)
167
+ ):
168
+ _merge(base[key], value)
169
+ else:
170
+ base[key] = value
@@ -0,0 +1,23 @@
1
+ {
2
+ "ang": ["Latn"],
3
+ "arc": ["Armi", "Syrc"],
4
+ "akk": ["Xsux"],
5
+ "cop": ["Copt", "Grek"],
6
+ "egy": ["Egyp", "Egyh", "Egyd"],
7
+ "fro": ["Latn"],
8
+ "got": ["Goth"],
9
+ "grc": ["Grek", "Linb"],
10
+ "hca": ["Latn"],
11
+ "hdn": ["Latn"],
12
+ "hit": ["Xsux"],
13
+ "mga": ["Ogam", "Latn"],
14
+ "mvi": ["Jpan"],
15
+ "mvb": ["Latn"],
16
+ "non": ["Runr", "Latn"],
17
+ "orv": ["Cyrs", "Cyrl"],
18
+ "peo": ["Xpeo"],
19
+ "phn": ["Phnx"],
20
+ "san": ["Deva", "Shar", "Brah"],
21
+ "sux": ["Xsux"],
22
+ "lat": ["Latn"]
23
+ }
@@ -0,0 +1,8 @@
1
+ [
2
+ "hun","kca","mns","ekk","fin","fkv","izh","krl","zkv","liv",
3
+ "olo","lud","fit","vep","vot","vro","mrj","mhr","myv","mdf",
4
+ "udm","koi","kpv","sia","smn","sjk","sjd","sms","sjt","sju",
5
+ "smj","sme","sje","sma","mtm","yrk","nio","enf","enh","rts",
6
+ "xas","sel"
7
+ ]
8
+
glfm/download.py ADDED
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ #
4
+ # GLFM Project
5
+ # Copyright (c) 2026 Tuomas Lähteenmäki
6
+ # Version 1.0.4
7
+ #
8
+ # https://codeberg.org/lahtis/GLFM
9
+ # Licensed under the MIT License.
10
+ #
11
+ # Lataa ladattavan datan (~/.cache/glfm/data/).
12
+ # Käyttö:
13
+ # python3 -m glfm.download
14
+ # python3 -m glfm.download --force
15
+ # python3 -m glfm.download --target /tmp/glfm-data
16
+ #
17
+
18
+ import argparse
19
+ import hashlib
20
+ import io
21
+ import logging
22
+ import shutil
23
+ import sys
24
+ import urllib.request
25
+ import zipfile
26
+ from pathlib import Path
27
+
28
+ log = logging.getLogger(__name__)
29
+
30
+
31
+ def download_data(force: bool = False) -> Path:
32
+ """
33
+ Lataa datan cacheen (~/.cache/glfm/data/).
34
+
35
+ Args:
36
+ force: pakota uudelleenlataus, vaikka data olisi jo olemassa
37
+
38
+ Returns:
39
+ Polku data-hakemistoon
40
+ """
41
+ from glfm.config import load_config
42
+ from glfm.paths import get_cache_dir
43
+
44
+ config = load_config()
45
+ cache = get_cache_dir() / "data"
46
+
47
+ if cache.exists() and not force:
48
+ log.info("Data jo olemassa: %s", cache)
49
+ return cache
50
+
51
+ url = config["download"]["url"]
52
+ sha256 = config["download"]["sha256"]
53
+ timeout = config["download"]["timeout"]
54
+
55
+ log.info("Ladataan: %s", url)
56
+
57
+ try:
58
+ with urllib.request.urlopen(url, timeout=timeout) as resp:
59
+ data = resp.read()
60
+ except Exception as e:
61
+ log.error("Lataus epäonnistui: %s", e)
62
+ raise
63
+
64
+ # Tarkista hash
65
+ if sha256:
66
+ h = hashlib.sha256(data).hexdigest()
67
+ if h != sha256:
68
+ raise RuntimeError(f"SHA256 ei täsmää: {h} != {sha256}")
69
+
70
+ # Pura
71
+ cache.parent.mkdir(parents=True, exist_ok=True)
72
+ tmp = cache.parent / "data.tmp"
73
+ if tmp.exists():
74
+ shutil.rmtree(tmp)
75
+ tmp.mkdir()
76
+
77
+ try:
78
+ with zipfile.ZipFile(io.BytesIO(data)) as z:
79
+ z.extractall(tmp)
80
+ except zipfile.BadZipFile as e:
81
+ shutil.rmtree(tmp, ignore_errors=True)
82
+ raise RuntimeError(f"Ladattu tiedosto ei ole kelvollinen zip: {e}")
83
+
84
+ # Atominen uudelleennimeäminen
85
+ if cache.exists():
86
+ shutil.rmtree(cache)
87
+ tmp.rename(cache)
88
+
89
+ log.info("Valmis: %s", cache)
90
+ return cache
91
+
92
+
93
+ def _data_size(path: Path) -> int:
94
+ """Palauttaa hakemiston koon tavuina."""
95
+ return sum(f.stat().st_size for f in path.rglob("*") if f.is_file())
96
+
97
+
98
+ def main():
99
+ parser = argparse.ArgumentParser(
100
+ description="Lataa GLFM-data (~/.cache/glfm/data/).",
101
+ )
102
+ parser.add_argument(
103
+ "--force", action="store_true",
104
+ help="Pakota uudelleenlataus (ohita olemassa oleva data)",
105
+ )
106
+ args = parser.parse_args()
107
+
108
+ logging.basicConfig(
109
+ level=logging.INFO,
110
+ format="%(levelname)s %(message)s",
111
+ )
112
+
113
+ try:
114
+ path = download_data(force=args.force)
115
+ except RuntimeError as e:
116
+ log.error("%s", e)
117
+ return 1
118
+
119
+ size_kt = _data_size(path) / 1024
120
+ print()
121
+ print(f"Data: {path}")
122
+ print(f"Koko: {size_kt:.1f} kt")
123
+ return 0
124
+
125
+
126
+ if __name__ == "__main__":
127
+ raise SystemExit(main())
@@ -0,0 +1,18 @@
1
+ # loaders/__init__.py
2
+ """
3
+ GLFM – datalähteiden latausmoduulit.
4
+
5
+ Jokainen loaderi lukee yhden datalähteen ja palauttaa jäsennellyn dictin.
6
+ """
7
+
8
+ __all__ = [
9
+ "load_cldr_likely_subtags",
10
+ "load_glottolog",
11
+ "load_iso_639",
12
+ "load_override_scripts",
13
+ "load_pos_stats",
14
+ "load_uralic_languages",
15
+ "load_wiktionary_languages",
16
+ "load_written_languages",
17
+ ]
18
+