fgsmdb 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.
- fgsmdb-0.1.0/PKG-INFO +168 -0
- fgsmdb-0.1.0/README.md +144 -0
- fgsmdb-0.1.0/fgsmdb/__init__.py +3 -0
- fgsmdb-0.1.0/fgsmdb/__main__.py +34 -0
- fgsmdb-0.1.0/fgsmdb/commands/__init__.py +1 -0
- fgsmdb-0.1.0/fgsmdb/commands/export.py +180 -0
- fgsmdb-0.1.0/fgsmdb/config.py +37 -0
- fgsmdb-0.1.0/fgsmdb/db.py +66 -0
- fgsmdb-0.1.0/fgsmdb/steps/__init__.py +1 -0
- fgsmdb-0.1.0/fgsmdb/steps/dirindex.py +147 -0
- fgsmdb-0.1.0/fgsmdb/steps/export_models.py +104 -0
- fgsmdb-0.1.0/fgsmdb/steps/export_stg.py +57 -0
- fgsmdb-0.1.0/fgsmdb/steps/gndelev.py +107 -0
- fgsmdb-0.1.0/fgsmdb/steps/rsync_distribute.py +84 -0
- fgsmdb-0.1.0/fgsmdb/steps/sanitize.py +32 -0
- fgsmdb-0.1.0/fgsmdb/steps/sync.py +150 -0
- fgsmdb-0.1.0/fgsmdb.egg-info/PKG-INFO +168 -0
- fgsmdb-0.1.0/fgsmdb.egg-info/SOURCES.txt +22 -0
- fgsmdb-0.1.0/fgsmdb.egg-info/dependency_links.txt +1 -0
- fgsmdb-0.1.0/fgsmdb.egg-info/entry_points.txt +2 -0
- fgsmdb-0.1.0/fgsmdb.egg-info/requires.txt +2 -0
- fgsmdb-0.1.0/fgsmdb.egg-info/top_level.txt +1 -0
- fgsmdb-0.1.0/pyproject.toml +41 -0
- fgsmdb-0.1.0/setup.cfg +4 -0
fgsmdb-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fgsmdb
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: FlightGear Scenemodels Database Tools
|
|
5
|
+
Author-email: Torsten Dreyer <torsten@t3r.de>
|
|
6
|
+
License-Expression: GPL-2.0-only
|
|
7
|
+
Project-URL: Homepage, https://github.com/FlightGear/scenemodels
|
|
8
|
+
Project-URL: Repository, https://github.com/FlightGear/scenemodels
|
|
9
|
+
Project-URL: Issues, https://github.com/FlightGear/scenemodels/issues
|
|
10
|
+
Keywords: flightgear,scenery,terrasync,export
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Games/Entertainment :: Simulation
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
Requires-Dist: psycopg2-binary>=2.9
|
|
23
|
+
Requires-Dist: python-dotenv>=1.0
|
|
24
|
+
|
|
25
|
+
# fgsmdb — FlightGear Scenemodels Database Tools
|
|
26
|
+
|
|
27
|
+
Command-line tools for the FlightGear scenemodels database.
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install fgsmdb
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Or install from source:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
cd export-legacy/fgsmdb
|
|
39
|
+
pip install .
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
For development:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install -e .
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Configuration
|
|
49
|
+
|
|
50
|
+
Copy `.env.example` to `.env` and fill in your values:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
cp .env.example .env
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
| Variable | Description | Default |
|
|
57
|
+
|----------|-------------|---------|
|
|
58
|
+
| `PGURI` | PostgreSQL connection URI (required) | — |
|
|
59
|
+
| `FGELEV_PATH` | Path to the fgelev binary | `/usr/local/bin/fgelev` |
|
|
60
|
+
| `FG_ROOT` | FlightGear data root (needed by fgelev) | `/app/data` |
|
|
61
|
+
| `FG_SCENERY` | Local scenery path (Terrain, Airports, etc.) | `./fgscenery` |
|
|
62
|
+
| `FG_SCENERY_EXPORT` | Working directory for exported files | `./export` |
|
|
63
|
+
| `GROUNDNETS_PATH` | Path to groundnets git repository | `./groundnets` |
|
|
64
|
+
| `MIN_UPDATES` | Minimum pending updates to trigger export | `1` |
|
|
65
|
+
| `SYNC_TARGETS` | Comma-separated rsync target URIs | — |
|
|
66
|
+
| `CDN_INVALIDATE_CMD` | Shell command to run after distribution | — |
|
|
67
|
+
|
|
68
|
+
For remote distribution, rsync uses your `~/.ssh/config` for host aliases,
|
|
69
|
+
usernames, and identity files.
|
|
70
|
+
|
|
71
|
+
## Usage
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
# Run the export pipeline
|
|
75
|
+
python3 -m fgsmdb export
|
|
76
|
+
|
|
77
|
+
# Or use the installed command
|
|
78
|
+
fgsmdb export
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Export Command
|
|
82
|
+
|
|
83
|
+
Exports the scenemodels database to a TerraSync-compatible file tree.
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# Full pipeline (all steps except distribute)
|
|
87
|
+
python3 -m fgsmdb export
|
|
88
|
+
|
|
89
|
+
# With a specific .env file
|
|
90
|
+
python3 -m fgsmdb export --env-file /path/to/.env
|
|
91
|
+
|
|
92
|
+
# Run specific steps only
|
|
93
|
+
python3 -m fgsmdb export --steps sanitize,export,mkdiridx
|
|
94
|
+
|
|
95
|
+
# Include distribution to mirrors
|
|
96
|
+
python3 -m fgsmdb export --steps sanitize,gndelev,export,mkdiridx,upload,distribute
|
|
97
|
+
|
|
98
|
+
# Skip the pending-updates threshold check
|
|
99
|
+
python3 -m fgsmdb export --skip-check
|
|
100
|
+
|
|
101
|
+
# Override the minimum updates threshold
|
|
102
|
+
python3 -m fgsmdb export --min-updates 10
|
|
103
|
+
|
|
104
|
+
# Enable verbose (debug) logging
|
|
105
|
+
python3 -m fgsmdb export -v
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Pipeline Steps
|
|
109
|
+
|
|
110
|
+
| Step | Description |
|
|
111
|
+
|------|-------------|
|
|
112
|
+
| `sanitize` | Fix data integrity: update tile numbers, nullify zero elevation offsets |
|
|
113
|
+
| `gndelev` | Compute ground elevation for new objects using fgelev |
|
|
114
|
+
| `export` | Export shared/per-hemisphere models and generate .stg files from the database |
|
|
115
|
+
| `mkdiridx` | Create `.dirindex` files for TerraSync and the Airports archive |
|
|
116
|
+
| `upload` | Sync exported files to the local scenery tree (hash-based, copies only changes) |
|
|
117
|
+
| `distribute` | Rsync scenery to remote mirror servers (parallel) |
|
|
118
|
+
|
|
119
|
+
By default, all steps except `distribute` are run. Distribution must be explicitly
|
|
120
|
+
requested via `--steps`.
|
|
121
|
+
|
|
122
|
+
Before any steps execute, the pipeline calls `fn_update_statistics()` and checks
|
|
123
|
+
whether the number of objects pending ground elevation meets the configured threshold.
|
|
124
|
+
Use `--skip-check` to bypass this gate.
|
|
125
|
+
|
|
126
|
+
### Command Line Options
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
python3 -m fgsmdb export [-h] [--env-file ENV_FILE] [--steps STEPS] [--all]
|
|
130
|
+
[--skip-check] [--min-updates N] [-v]
|
|
131
|
+
|
|
132
|
+
Options:
|
|
133
|
+
--env-file ENV_FILE Path to .env file (default: auto-detect)
|
|
134
|
+
--steps STEPS Comma-separated steps: sanitize, gndelev, export,
|
|
135
|
+
mkdiridx, upload, distribute
|
|
136
|
+
--all Run all steps (same as default, without distribute)
|
|
137
|
+
--skip-check Skip the pending-updates threshold check
|
|
138
|
+
--min-updates N Override the MIN_UPDATES threshold from .env
|
|
139
|
+
-v, --verbose Enable debug logging
|
|
140
|
+
-h, --help Show help message
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Output Structure
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
fgscenery/
|
|
147
|
+
├── .dirindex
|
|
148
|
+
├── Airports_archive.tgz
|
|
149
|
+
├── SharedModels.txz
|
|
150
|
+
├── Airports/
|
|
151
|
+
│ ├── .dirindex
|
|
152
|
+
│ └── ...
|
|
153
|
+
├── Models/
|
|
154
|
+
│ ├── .dirindex
|
|
155
|
+
│ └── <group_path>/
|
|
156
|
+
├── Objects/
|
|
157
|
+
│ ├── .dirindex
|
|
158
|
+
│ └── <e|w>NNN<n|s>NN/
|
|
159
|
+
│ └── <e|w>NNN<n|s>NN/
|
|
160
|
+
│ ├── <tile>.stg
|
|
161
|
+
│ └── <model_files>
|
|
162
|
+
└── Terrain/
|
|
163
|
+
└── .dirindex
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## License
|
|
167
|
+
|
|
168
|
+
GNU General Public License v2.0 — see [LICENCE](../LICENCE).
|
fgsmdb-0.1.0/README.md
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# fgsmdb — FlightGear Scenemodels Database Tools
|
|
2
|
+
|
|
3
|
+
Command-line tools for the FlightGear scenemodels database.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install fgsmdb
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or install from source:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
cd export-legacy/fgsmdb
|
|
15
|
+
pip install .
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
For development:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install -e .
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Configuration
|
|
25
|
+
|
|
26
|
+
Copy `.env.example` to `.env` and fill in your values:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
cp .env.example .env
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
| Variable | Description | Default |
|
|
33
|
+
|----------|-------------|---------|
|
|
34
|
+
| `PGURI` | PostgreSQL connection URI (required) | — |
|
|
35
|
+
| `FGELEV_PATH` | Path to the fgelev binary | `/usr/local/bin/fgelev` |
|
|
36
|
+
| `FG_ROOT` | FlightGear data root (needed by fgelev) | `/app/data` |
|
|
37
|
+
| `FG_SCENERY` | Local scenery path (Terrain, Airports, etc.) | `./fgscenery` |
|
|
38
|
+
| `FG_SCENERY_EXPORT` | Working directory for exported files | `./export` |
|
|
39
|
+
| `GROUNDNETS_PATH` | Path to groundnets git repository | `./groundnets` |
|
|
40
|
+
| `MIN_UPDATES` | Minimum pending updates to trigger export | `1` |
|
|
41
|
+
| `SYNC_TARGETS` | Comma-separated rsync target URIs | — |
|
|
42
|
+
| `CDN_INVALIDATE_CMD` | Shell command to run after distribution | — |
|
|
43
|
+
|
|
44
|
+
For remote distribution, rsync uses your `~/.ssh/config` for host aliases,
|
|
45
|
+
usernames, and identity files.
|
|
46
|
+
|
|
47
|
+
## Usage
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# Run the export pipeline
|
|
51
|
+
python3 -m fgsmdb export
|
|
52
|
+
|
|
53
|
+
# Or use the installed command
|
|
54
|
+
fgsmdb export
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Export Command
|
|
58
|
+
|
|
59
|
+
Exports the scenemodels database to a TerraSync-compatible file tree.
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# Full pipeline (all steps except distribute)
|
|
63
|
+
python3 -m fgsmdb export
|
|
64
|
+
|
|
65
|
+
# With a specific .env file
|
|
66
|
+
python3 -m fgsmdb export --env-file /path/to/.env
|
|
67
|
+
|
|
68
|
+
# Run specific steps only
|
|
69
|
+
python3 -m fgsmdb export --steps sanitize,export,mkdiridx
|
|
70
|
+
|
|
71
|
+
# Include distribution to mirrors
|
|
72
|
+
python3 -m fgsmdb export --steps sanitize,gndelev,export,mkdiridx,upload,distribute
|
|
73
|
+
|
|
74
|
+
# Skip the pending-updates threshold check
|
|
75
|
+
python3 -m fgsmdb export --skip-check
|
|
76
|
+
|
|
77
|
+
# Override the minimum updates threshold
|
|
78
|
+
python3 -m fgsmdb export --min-updates 10
|
|
79
|
+
|
|
80
|
+
# Enable verbose (debug) logging
|
|
81
|
+
python3 -m fgsmdb export -v
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Pipeline Steps
|
|
85
|
+
|
|
86
|
+
| Step | Description |
|
|
87
|
+
|------|-------------|
|
|
88
|
+
| `sanitize` | Fix data integrity: update tile numbers, nullify zero elevation offsets |
|
|
89
|
+
| `gndelev` | Compute ground elevation for new objects using fgelev |
|
|
90
|
+
| `export` | Export shared/per-hemisphere models and generate .stg files from the database |
|
|
91
|
+
| `mkdiridx` | Create `.dirindex` files for TerraSync and the Airports archive |
|
|
92
|
+
| `upload` | Sync exported files to the local scenery tree (hash-based, copies only changes) |
|
|
93
|
+
| `distribute` | Rsync scenery to remote mirror servers (parallel) |
|
|
94
|
+
|
|
95
|
+
By default, all steps except `distribute` are run. Distribution must be explicitly
|
|
96
|
+
requested via `--steps`.
|
|
97
|
+
|
|
98
|
+
Before any steps execute, the pipeline calls `fn_update_statistics()` and checks
|
|
99
|
+
whether the number of objects pending ground elevation meets the configured threshold.
|
|
100
|
+
Use `--skip-check` to bypass this gate.
|
|
101
|
+
|
|
102
|
+
### Command Line Options
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
python3 -m fgsmdb export [-h] [--env-file ENV_FILE] [--steps STEPS] [--all]
|
|
106
|
+
[--skip-check] [--min-updates N] [-v]
|
|
107
|
+
|
|
108
|
+
Options:
|
|
109
|
+
--env-file ENV_FILE Path to .env file (default: auto-detect)
|
|
110
|
+
--steps STEPS Comma-separated steps: sanitize, gndelev, export,
|
|
111
|
+
mkdiridx, upload, distribute
|
|
112
|
+
--all Run all steps (same as default, without distribute)
|
|
113
|
+
--skip-check Skip the pending-updates threshold check
|
|
114
|
+
--min-updates N Override the MIN_UPDATES threshold from .env
|
|
115
|
+
-v, --verbose Enable debug logging
|
|
116
|
+
-h, --help Show help message
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Output Structure
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
fgscenery/
|
|
123
|
+
├── .dirindex
|
|
124
|
+
├── Airports_archive.tgz
|
|
125
|
+
├── SharedModels.txz
|
|
126
|
+
├── Airports/
|
|
127
|
+
│ ├── .dirindex
|
|
128
|
+
│ └── ...
|
|
129
|
+
├── Models/
|
|
130
|
+
│ ├── .dirindex
|
|
131
|
+
│ └── <group_path>/
|
|
132
|
+
├── Objects/
|
|
133
|
+
│ ├── .dirindex
|
|
134
|
+
│ └── <e|w>NNN<n|s>NN/
|
|
135
|
+
│ └── <e|w>NNN<n|s>NN/
|
|
136
|
+
│ ├── <tile>.stg
|
|
137
|
+
│ └── <model_files>
|
|
138
|
+
└── Terrain/
|
|
139
|
+
└── .dirindex
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## License
|
|
143
|
+
|
|
144
|
+
GNU General Public License v2.0 — see [LICENCE](../LICENCE).
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""CLI entry point for fgsmdb.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
python3 -m fgsmdb export [options]
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
from .commands import export
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main():
|
|
14
|
+
parser = argparse.ArgumentParser(
|
|
15
|
+
prog="fgsmdb",
|
|
16
|
+
description="FlightGear Scenemodels Database Tools",
|
|
17
|
+
)
|
|
18
|
+
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
|
19
|
+
|
|
20
|
+
# Register the export subcommand
|
|
21
|
+
export.register(subparsers)
|
|
22
|
+
|
|
23
|
+
args = parser.parse_args()
|
|
24
|
+
|
|
25
|
+
if args.command is None:
|
|
26
|
+
parser.print_help()
|
|
27
|
+
sys.exit(1)
|
|
28
|
+
|
|
29
|
+
# Dispatch to the command handler
|
|
30
|
+
args.func(args)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
if __name__ == "__main__":
|
|
34
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# fgsmdb command modules
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""Export command — orchestrates the full scenemodels export pipeline.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
python3 -m fgsmdb export [--env-file .env] [--steps sanitize,gndelev,export,mkdiridx,upload,distribute]
|
|
5
|
+
python3 -m fgsmdb export --all
|
|
6
|
+
python3 -m fgsmdb export --steps export,mkdiridx
|
|
7
|
+
|
|
8
|
+
Available steps:
|
|
9
|
+
sanitize - Fix data integrity (tile numbers, elevation offsets)
|
|
10
|
+
gndelev - Compute ground elevations using fgelev
|
|
11
|
+
export - Export models and STG files from the database
|
|
12
|
+
mkdiridx - Create .dirindex files and Airports archive
|
|
13
|
+
upload - Sync export directory to local scenery tree
|
|
14
|
+
distribute - Rsync scenery to mirror servers
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
import os
|
|
19
|
+
import tarfile
|
|
20
|
+
|
|
21
|
+
from ..config import load_config
|
|
22
|
+
from ..db import Database
|
|
23
|
+
from ..steps import sanitize, gndelev, export_models, export_stg, dirindex, sync, rsync_distribute
|
|
24
|
+
|
|
25
|
+
ALL_STEPS = ["sanitize", "gndelev", "export", "mkdiridx", "upload"]
|
|
26
|
+
VALID_STEPS = ALL_STEPS + ["distribute"]
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def register(subparsers):
|
|
32
|
+
"""Register the export subcommand with the argument parser."""
|
|
33
|
+
parser = subparsers.add_parser(
|
|
34
|
+
"export",
|
|
35
|
+
help="Export scenemodels database to TerraSync file tree",
|
|
36
|
+
description="Export the FlightGear scenemodels database into a "
|
|
37
|
+
"TerraSync-compatible file tree and optionally distribute to mirrors.",
|
|
38
|
+
)
|
|
39
|
+
parser.add_argument(
|
|
40
|
+
"--env-file",
|
|
41
|
+
default=None,
|
|
42
|
+
help="Path to .env file (default: auto-detect)",
|
|
43
|
+
)
|
|
44
|
+
parser.add_argument(
|
|
45
|
+
"--steps",
|
|
46
|
+
default=None,
|
|
47
|
+
help="Comma-separated list of steps to run (default: all except distribute)",
|
|
48
|
+
)
|
|
49
|
+
parser.add_argument(
|
|
50
|
+
"--all",
|
|
51
|
+
action="store_true",
|
|
52
|
+
help="Run all steps (equivalent to: %s)" % ",".join(ALL_STEPS),
|
|
53
|
+
)
|
|
54
|
+
parser.add_argument(
|
|
55
|
+
"--skip-check",
|
|
56
|
+
action="store_true",
|
|
57
|
+
help="Skip the pending-updates threshold check",
|
|
58
|
+
)
|
|
59
|
+
parser.add_argument(
|
|
60
|
+
"--min-updates",
|
|
61
|
+
type=int,
|
|
62
|
+
default=None,
|
|
63
|
+
help="Override MIN_UPDATES threshold from .env",
|
|
64
|
+
)
|
|
65
|
+
parser.add_argument(
|
|
66
|
+
"--verbose", "-v",
|
|
67
|
+
action="store_true",
|
|
68
|
+
help="Enable debug logging",
|
|
69
|
+
)
|
|
70
|
+
parser.set_defaults(func=run)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def run(args):
|
|
74
|
+
"""Execute the export pipeline."""
|
|
75
|
+
# Setup logging
|
|
76
|
+
log_level = logging.DEBUG if args.verbose else logging.INFO
|
|
77
|
+
logging.basicConfig(
|
|
78
|
+
level=log_level,
|
|
79
|
+
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
80
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Load configuration
|
|
84
|
+
config = load_config(args.env_file)
|
|
85
|
+
|
|
86
|
+
# Determine which steps to run
|
|
87
|
+
if args.steps:
|
|
88
|
+
steps = [s.strip() for s in args.steps.split(",")]
|
|
89
|
+
for s in steps:
|
|
90
|
+
if s not in VALID_STEPS:
|
|
91
|
+
logger.error("Unknown step: %s. Valid steps: %s", s, ", ".join(VALID_STEPS))
|
|
92
|
+
raise SystemExit(1)
|
|
93
|
+
else:
|
|
94
|
+
steps = ALL_STEPS
|
|
95
|
+
|
|
96
|
+
# Override min_updates if specified
|
|
97
|
+
min_updates = args.min_updates if args.min_updates is not None else config["min_updates"]
|
|
98
|
+
|
|
99
|
+
# Connect to the database
|
|
100
|
+
db = Database(config["pguri"])
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
# Always update statistics first
|
|
104
|
+
logger.info("Updating statistics...")
|
|
105
|
+
db.execute("SELECT fn_update_statistics()")
|
|
106
|
+
|
|
107
|
+
# Check if there are enough pending updates
|
|
108
|
+
if not args.skip_check:
|
|
109
|
+
rows = db.execute(
|
|
110
|
+
"SELECT count(*) AS cnt FROM fgs_objects WHERE ob_gndelev = -9999"
|
|
111
|
+
)
|
|
112
|
+
count = rows[0]["cnt"] if rows else 0
|
|
113
|
+
logger.info("Pending model updates: %d (minimum: %d)", count, min_updates)
|
|
114
|
+
if count < min_updates:
|
|
115
|
+
logger.info("Not enough pending updates. Exiting.")
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
# Run pipeline steps
|
|
119
|
+
if "sanitize" in steps:
|
|
120
|
+
sanitize.run(db)
|
|
121
|
+
|
|
122
|
+
if "gndelev" in steps:
|
|
123
|
+
gndelev.run(
|
|
124
|
+
db,
|
|
125
|
+
fgelev_path=config["fgelev_path"],
|
|
126
|
+
fg_root=config["fg_root"],
|
|
127
|
+
fg_scenery=config["fg_scenery"],
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
if "export" in steps:
|
|
131
|
+
export_models.run(db, target=config["fg_scenery_export"])
|
|
132
|
+
export_stg.run(db, target=config["fg_scenery_export"])
|
|
133
|
+
|
|
134
|
+
if "mkdiridx" in steps:
|
|
135
|
+
dirindex.create_root_dirindex(
|
|
136
|
+
fg_scenery=config["fg_scenery"],
|
|
137
|
+
fg_scenery_export=config["fg_scenery_export"],
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
if "upload" in steps:
|
|
141
|
+
sync.run(
|
|
142
|
+
fg_scenery_export=config["fg_scenery_export"],
|
|
143
|
+
fg_scenery=config["fg_scenery"],
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# Update statistics after export
|
|
147
|
+
if any(s in steps for s in ("export", "upload")):
|
|
148
|
+
db.execute("SELECT fn_update_statistics()")
|
|
149
|
+
|
|
150
|
+
# Create SharedModels tarball
|
|
151
|
+
if "upload" in steps:
|
|
152
|
+
_create_shared_models_tarball(config["fg_scenery"])
|
|
153
|
+
|
|
154
|
+
# Distribution (rsync to mirrors)
|
|
155
|
+
if "distribute" in steps:
|
|
156
|
+
rsync_distribute.run(
|
|
157
|
+
fg_scenery=config["fg_scenery"],
|
|
158
|
+
sync_targets=config["sync_targets"],
|
|
159
|
+
cdn_invalidate_cmd=config["cdn_invalidate_cmd"],
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
logger.info("Export pipeline complete.")
|
|
163
|
+
|
|
164
|
+
finally:
|
|
165
|
+
db.close()
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _create_shared_models_tarball(fg_scenery: str):
|
|
169
|
+
"""Create SharedModels.txz from the Models directory."""
|
|
170
|
+
models_path = os.path.join(fg_scenery, "Models")
|
|
171
|
+
output_path = os.path.join(fg_scenery, "SharedModels.txz")
|
|
172
|
+
|
|
173
|
+
if not os.path.isdir(models_path):
|
|
174
|
+
logger.warning("No Models directory at %s, skipping tarball.", models_path)
|
|
175
|
+
return
|
|
176
|
+
|
|
177
|
+
logger.info("Creating SharedModels.txz...")
|
|
178
|
+
with tarfile.open(output_path, "w:xz") as tar:
|
|
179
|
+
tar.add(models_path, arcname="Models")
|
|
180
|
+
logger.info("SharedModels.txz created at %s", output_path)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Configuration loader — reads .env and provides settings to all modules."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from dotenv import load_dotenv
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def load_config(env_file: str | None = None) -> dict:
|
|
8
|
+
"""Load configuration from .env file and environment variables.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
env_file: Path to .env file. If None, searches in current directory
|
|
12
|
+
and parent directories.
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
Dictionary with all configuration values.
|
|
16
|
+
"""
|
|
17
|
+
if env_file:
|
|
18
|
+
load_dotenv(env_file)
|
|
19
|
+
else:
|
|
20
|
+
load_dotenv()
|
|
21
|
+
|
|
22
|
+
config = {
|
|
23
|
+
"pguri": os.environ["PGURI"],
|
|
24
|
+
"fgelev_path": os.environ.get("FGELEV_PATH", "/usr/local/bin/fgelev"),
|
|
25
|
+
"fg_root": os.environ.get("FG_ROOT", "/app/data"),
|
|
26
|
+
"fg_scenery": os.environ.get("FG_SCENERY", "./fgscenery"),
|
|
27
|
+
"fg_scenery_export": os.environ.get("FG_SCENERY_EXPORT", "./export"),
|
|
28
|
+
"groundnets_path": os.environ.get("GROUNDNETS_PATH", "./groundnets"),
|
|
29
|
+
"min_updates": int(os.environ.get("MIN_UPDATES", "1")),
|
|
30
|
+
"sync_targets": [
|
|
31
|
+
t.strip()
|
|
32
|
+
for t in os.environ.get("SYNC_TARGETS", "").split(",")
|
|
33
|
+
if t.strip()
|
|
34
|
+
],
|
|
35
|
+
"cdn_invalidate_cmd": os.environ.get("CDN_INVALIDATE_CMD", ""),
|
|
36
|
+
}
|
|
37
|
+
return config
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Database connection helper with automatic reconnect."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import psycopg2
|
|
5
|
+
import psycopg2.extras
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Database:
|
|
11
|
+
"""Thin wrapper around psycopg2 with reconnect logic."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, pguri: str):
|
|
14
|
+
self.pguri = pguri
|
|
15
|
+
self._conn = None
|
|
16
|
+
|
|
17
|
+
def connect(self):
|
|
18
|
+
"""Establish database connection."""
|
|
19
|
+
if self._conn is None or self._conn.closed:
|
|
20
|
+
logger.info("Connecting to database...")
|
|
21
|
+
self._conn = psycopg2.connect(self.pguri)
|
|
22
|
+
self._conn.autocommit = True
|
|
23
|
+
return self._conn
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def conn(self):
|
|
27
|
+
return self.connect()
|
|
28
|
+
|
|
29
|
+
def cursor(self):
|
|
30
|
+
"""Return a DictCursor, reconnecting if needed."""
|
|
31
|
+
return self.conn.cursor(cursor_factory=psycopg2.extras.DictCursor)
|
|
32
|
+
|
|
33
|
+
def execute(self, sql: str, params=None, retries: int = 3):
|
|
34
|
+
"""Execute a query with automatic reconnect on OperationalError.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
List of result rows, or empty list for non-SELECT statements.
|
|
38
|
+
"""
|
|
39
|
+
for attempt in range(retries):
|
|
40
|
+
try:
|
|
41
|
+
cur = self.cursor()
|
|
42
|
+
cur.execute(sql, params)
|
|
43
|
+
if cur.description:
|
|
44
|
+
return cur.fetchall()
|
|
45
|
+
return []
|
|
46
|
+
except psycopg2.OperationalError:
|
|
47
|
+
logger.warning(
|
|
48
|
+
"Database connection lost (attempt %d/%d), reconnecting...",
|
|
49
|
+
attempt + 1,
|
|
50
|
+
retries,
|
|
51
|
+
)
|
|
52
|
+
self._conn = None
|
|
53
|
+
if attempt == retries - 1:
|
|
54
|
+
raise
|
|
55
|
+
|
|
56
|
+
def execute_batch(self, statements: list[str]):
|
|
57
|
+
"""Execute multiple SQL statements in sequence."""
|
|
58
|
+
cur = self.cursor()
|
|
59
|
+
for stmt in statements:
|
|
60
|
+
cur.execute(stmt)
|
|
61
|
+
|
|
62
|
+
def close(self):
|
|
63
|
+
"""Close the database connection."""
|
|
64
|
+
if self._conn and not self._conn.closed:
|
|
65
|
+
self._conn.close()
|
|
66
|
+
self._conn = None
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# fgsmdb pipeline step modules
|