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 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,3 @@
1
+ """fgsmdb — FlightGear Scenemodels Database Tools."""
2
+
3
+ __version__ = "0.1.0"
@@ -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