[Git][debian-gis-team/pyorbital][master] 6 commits: New upstream version 1.11.0
Antonio Valentino (@antonio.valentino)
gitlab at salsa.debian.org
Sat Nov 22 09:38:56 GMT 2025
Antonio Valentino pushed to branch master at Debian GIS Project / pyorbital
Commits:
b61b859b by Antonio Valentino at 2025-11-22T09:14:12+00:00
New upstream version 1.11.0
- - - - -
c02a095c by Antonio Valentino at 2025-11-22T09:14:13+00:00
Update upstream source from tag 'upstream/1.11.0'
Update to upstream version '1.11.0'
with Debian dir fbb8ebe8e1add92873634d4d007e390d469f9a72
- - - - -
1d831785 by Antonio Valentino at 2025-11-22T09:19:33+00:00
New upstream release
- - - - -
e5668804 by Antonio Valentino at 2025-11-22T09:19:38+00:00
Refresh all patches and drop 0003-Pytest-8.4-compat.patch
- - - - -
233a088f by Antonio Valentino at 2025-11-22T09:23:48+00:00
Add dependency on pythone-donfig
- - - - -
5ecfeb41 by Antonio Valentino at 2025-11-22T09:24:12+00:00
Set distribution to unstable
- - - - -
16 changed files:
- .github/workflows/ci.yaml
- .github/workflows/deploy-sdist.yaml
- .gitignore
- .pre-commit-config.yaml
- CHANGELOG.md
- debian/changelog
- debian/control
- − debian/patches/0003-Pytest-8.4-compat.patch
- debian/patches/series
- + pyorbital/config.py
- pyorbital/etc/platforms.txt
- pyorbital/orbital.py
- + pyorbital/tests/test_orbit_elements.py
- pyorbital/tests/test_tlefile.py
- pyorbital/tlefile.py
- pyproject.toml
Changes:
=====================================
.github/workflows/ci.yaml
=====================================
@@ -28,7 +28,7 @@ jobs:
steps:
- name: Checkout source
- uses: actions/checkout at v4
+ uses: actions/checkout at v5
- name: Setup Conda Environment
uses: conda-incubator/setup-miniconda at v3
@@ -66,6 +66,7 @@ jobs:
python -m pip install \
--index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple/ \
--trusted-host pypi.anaconda.org \
+ --only-binary ":all:" \
--no-deps --pre --upgrade \
matplotlib \
numpy \
=====================================
.github/workflows/deploy-sdist.yaml
=====================================
@@ -11,7 +11,7 @@ jobs:
steps:
- name: Checkout source
- uses: actions/checkout at v4
+ uses: actions/checkout at v5
- name: Create sdist
shell: bash -l {0}
@@ -21,7 +21,7 @@ jobs:
- name: Publish package to PyPI
if: github.event.action == 'published'
- uses: pypa/gh-action-pypi-publish at v1.12.4
+ uses: pypa/gh-action-pypi-publish at v1.13.0
with:
user: __token__
password: ${{ secrets.pypi_password }}
=====================================
.gitignore
=====================================
@@ -40,4 +40,8 @@ nosetests.xml
# rope
.ropeproject
+# Virtual environment
+venv/
+.venv/
+
pyorbital/version.py
=====================================
.pre-commit-config.yaml
=====================================
@@ -3,11 +3,11 @@ fail_fast: false
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
- rev: 'v0.11.12'
+ rev: 'v0.14.3'
hooks:
- id: ruff
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v5.0.0
+ rev: v6.0.0
hooks:
- id: trailing-whitespace
exclude: pyorbital/tests/SGP4-VER.TLE
@@ -15,12 +15,12 @@ repos:
- id: check-yaml
args: [--unsafe]
- repo: https://github.com/PyCQA/bandit
- rev: '1.8.3' # Update me!
+ rev: '1.8.6' # Update me!
hooks:
- id: bandit
args: [--ini, .bandit]
- repo: https://github.com/pre-commit/mirrors-mypy
- rev: 'v1.16.0' # Use the sha / tag you want to point at
+ rev: 'v1.18.2' # Use the sha / tag you want to point at
hooks:
- id: mypy
additional_dependencies:
@@ -30,7 +30,7 @@ repos:
- types-requests
args: ["--python-version", "3.10", "--ignore-missing-imports"]
- repo: https://github.com/pycqa/isort
- rev: 6.0.1
+ rev: 7.0.0
hooks:
- id: isort
language_version: python3
=====================================
CHANGELOG.md
=====================================
@@ -1,3 +1,38 @@
+###############################################################################
+## Version 1.11.0 (2025/11/18)
+
+### Issues Closed
+
+* [Issue 203](https://github.com/pytroll/pyorbital/issues/203) - Coordinate system and units for `get_position` are unspecified
+* [Issue 145](https://github.com/pytroll/pyorbital/issues/145) - Geostationary satellite scan geometry example
+* [Issue 111](https://github.com/pytroll/pyorbital/issues/111) - Documentation refers to PPP_CONFIG_DIR
+* [Issue 74](https://github.com/pytroll/pyorbital/issues/74) - ValueError in case of empty or missing TLE files ([PR 205](https://github.com/pytroll/pyorbital/pull/205) by [@pnuu](https://github.com/pnuu))
+* [Issue 61](https://github.com/pytroll/pyorbital/issues/61) - Test data never makes it in sdist/bdist packages
+* [Issue 43](https://github.com/pytroll/pyorbital/issues/43) - Satellite elevation/azimuth wrong
+
+In this release 6 issues were closed.
+
+### Pull Requests Merged
+
+#### Bugs fixed
+
+* [PR 214](https://github.com/pytroll/pyorbital/pull/214) - Fix TLE writing by adding a newline after the last TLE entry
+* [PR 213](https://github.com/pytroll/pyorbital/pull/213) - Fix timeout preventing storage of TLEs
+
+#### Features added
+
+* [PR 213](https://github.com/pytroll/pyorbital/pull/213) - Fix timeout preventing storage of TLEs
+* [PR 211](https://github.com/pytroll/pyorbital/pull/211) - Rename 'excentricity' properties to 'eccentricity'
+* [PR 210](https://github.com/pytroll/pyorbital/pull/210) - Start deprecating automatic tle downloads
+* [PR 205](https://github.com/pytroll/pyorbital/pull/205) - Add fallback when `TLES` environment variable not listing any files ([74](https://github.com/pytroll/pyorbital/issues/74), [74](https://github.com/pytroll/pyorbital/issues/74))
+* [PR 204](https://github.com/pytroll/pyorbital/pull/204) - Extend `OrbitElements` Class with New Methods and Add Pytest Coverage
+* [PR 202](https://github.com/pytroll/pyorbital/pull/202) - Move import of tlefile inline to avoid import-time side effects
+
+In this release 8 pull requests were closed.
+
+###############################################################################
+
+
## Version 1.10.2 (2025/06/09)
=====================================
debian/changelog
=====================================
@@ -1,16 +1,18 @@
-pyorbital (1.10.2-3) UNRELEASED; urgency=medium
+pyorbital (1.11.0-1) unstable; urgency=medium
[ Antonio Valentino ]
+ * New upstream release.
* debian/patches:
- - 0003-Pytest-3.4-compat.patch renamed into
- 0003-Pytest-8.4-compat.patch.
+ - Drop 0003-Pytest-8.4-compat.patch, applied upstream.
+ * debian/control:
+ - Add dependency on python3-donfig.
[ Bas Couwenberg ]
* Update lintian overrides.
* Drop Rules-Requires-Root: no, default since dpkg 1.22.13.
* Use test-build-validate-cleanup instead of test-build-twice.
- -- Antonio Valentino <antonio.valentino at tiscali.it> Sun, 07 Sep 2025 10:39:35 +0000
+ -- Antonio Valentino <antonio.valentino at tiscali.it> Sat, 22 Nov 2025 09:23:51 +0000
pyorbital (1.10.2-2) unstable; urgency=medium
=====================================
debian/control
=====================================
@@ -13,6 +13,7 @@ Build-Depends: debhelper-compat (= 13),
pybuild-plugin-pyproject,
python3-all,
python3-dask,
+ python3-donfig,
python3-hatchling,
python3-hatch-vcs,
python3-numpy,
=====================================
debian/patches/0003-Pytest-8.4-compat.patch deleted
=====================================
@@ -1,21 +0,0 @@
-From: Antonio Valentino <antonio.valentino at tiscali.it>
-Date: Sun, 7 Sep 2025 08:45:54 +0000
-Subject: Pytest 8.4 compat
-
-Forwarded: https://github.com/pytroll/pyorbital/pull/200
----
- pyorbital/tests/test_tlefile.py | 1 +
- 1 file changed, 1 insertion(+)
-
-diff --git a/pyorbital/tests/test_tlefile.py b/pyorbital/tests/test_tlefile.py
-index 6e50df5..915156e 100644
---- a/pyorbital/tests/test_tlefile.py
-+++ b/pyorbital/tests/test_tlefile.py
-@@ -721,6 +721,7 @@ class TestSQLiteTLE(unittest.TestCase):
- """Clean temporary files."""
- with suppress(PermissionError, NotADirectoryError):
- self.temp_dir.cleanup()
-+ self.db.close()
-
- def test_init(self):
- """Test that the init did what it should have."""
=====================================
debian/patches/series
=====================================
@@ -1,3 +1,2 @@
0001-Fix-pricacy-breach.patch
0002-Reproducible-build.patch
-0003-Pytest-8.4-compat.patch
=====================================
pyorbital/config.py
=====================================
@@ -0,0 +1,4 @@
+"""Set up the config object."""
+from donfig import Config
+
+config = Config("pyorbital")
=====================================
pyorbital/etc/platforms.txt
=====================================
@@ -48,6 +48,31 @@ INSAT-3C 27298
INSAT-3D 39216
JASON-2 33105
Kalpana-1 27525
+KINEIS-5A 62084
+KINEIS-4E 63300
+KINEIS-3B 61224
+KINEIS-2D 62930
+KINEIS-5E 62083
+KINEIS-3A 61223
+KINEIS-2C 62929
+KINEIS-1A 60084
+KINEIS-5D 62082
+KINEIS-3E 61222
+KINEIS-4B 63304
+KINEIS-1E 60083
+KINEIS-5C 62081
+KINEIS-3D 61221
+KINEIS-4A 63303
+KINEIS-1D 60082
+KINEIS-2B 62934
+KINEIS-3C 61220
+KINEIS-4D 63302
+KINEIS-1C 60081
+KINEIS-2A 62932
+KINEIS-5B 62085
+KINEIS-4C 63301
+KINEIS-1B 60079
+KINEIS-2E 62931
Landsat-7 25682
Landsat-8 39084
Meteosat-7 24932
=====================================
pyorbital/orbital.py
=====================================
@@ -32,7 +32,7 @@ from functools import partial
import numpy as np
from scipy import optimize
-from pyorbital import astronomy, dt2np, tlefile
+from pyorbital import astronomy, dt2np
try:
import dask.array as da
@@ -155,6 +155,8 @@ class Orbital(object):
def __init__(self, satellite, tle_file=None, line1=None, line2=None):
"""Initialize the class."""
+ from pyorbital import tlefile
+
satellite = satellite.upper()
self.satellite_name = satellite
self.tle = tlefile.read(satellite, tle_file=tle_file,
@@ -568,46 +570,135 @@ def _get_max_parab(fun, start, end, tol=0.01):
f_a, f_b, f_c = fun(a), f_x, fun(c)
-class OrbitElements(object):
+class OrbitElements:
"""Class holding the orbital elements."""
def __init__(self, tle):
"""Initialize the class."""
self.epoch = tle.epoch
- self.excentricity = tle.excentricity
+ self.eccentricity = tle.eccentricity
self.inclination = np.deg2rad(tle.inclination)
self.right_ascension = np.deg2rad(tle.right_ascension)
self.arg_perigee = np.deg2rad(tle.arg_perigee)
self.mean_anomaly = np.deg2rad(tle.mean_anomaly)
- self.mean_motion = tle.mean_motion * (np.pi * 2 / XMNPDA)
- self.mean_motion_derivative = tle.mean_motion_derivative * \
- np.pi * 2 / XMNPDA ** 2
- self.mean_motion_sec_derivative = tle.mean_motion_sec_derivative * \
- np.pi * 2 / XMNPDA ** 3
+ self.mean_motion = tle.mean_motion * (2 * np.pi / XMNPDA)
+ self.mean_motion_derivative = tle.mean_motion_derivative * (2 * np.pi / XMNPDA**2)
+ self.mean_motion_sec_derivative = tle.mean_motion_sec_derivative * (2 * np.pi / XMNPDA**3)
self.bstar = tle.bstar * AE
- self.original_mean_motion, self.semi_major_axis = \
- self._calculate_mean_motion_and_semi_major_axis()
- self._calculate_mean_motion_and_semi_major_axis()
+ self.original_mean_motion, self.semi_major_axis = self._calculate_mean_motion_and_semi_major_axis()
+
+ self.period = 2 * np.pi / self.original_mean_motion
+ self.right_ascension_lon = self.right_ascension - astronomy.gmst(self.epoch)
+ self.right_ascension_lon = np.fmod(self.right_ascension_lon + np.pi, 2 * np.pi) - np.pi
+
+ @property
+ def excentricity(self):
+ """Get 'eccentricity' using legacy 'excentricity' name."""
+ warnings.warn("The 'eccentricity' property is deprecated in favor of 'eccentricity'", stacklevel=2)
+ return self.eccentricity
+
+ @property
+ def apogee(self):
+ """Compute apogee altitude in kilometers."""
+ return ((self.semi_major_axis * (1 + self.eccentricity)) / AE - AE) * XKMPER
+
+ @property
+ def perigee(self):
+ """Compute perigee altitude in kilometers."""
+ return ((self.semi_major_axis * (1 - self.eccentricity)) / AE - AE) * XKMPER
+
+ @property
+ def is_circular(self):
+ """Check if orbit is nearly circular."""
+ return self.eccentricity < 1e-3
+
+ @property
+ def is_retrograde(self):
+ """Check if orbit is retrograde (inclination > 90°)."""
+ return self.inclination > np.pi / 2
+
+ def _get_true_anomaly(self):
+ """Computes the True Anomaly (nu) from Mean Anomaly (M) and Eccentricity (e)."""
+ M = self.mean_anomaly
+ e = self.eccentricity
+ E = M # Initial guess for Eccentric Anomaly (E)
+
+ # Iteratively solve Kepler's Equation (M = E - e*sin(E))
+ for _ in range(10): # Max 10 iterations
+ f = E - e * np.sin(E) - M
+ f_prime = 1 - e * np.cos(E)
+ E_new = E - f / f_prime
+ if np.abs(E_new - E) < 1e-8: # Tolerance
+ E = E_new
+ break
+ E = E_new
+
+ # Convert Eccentric Anomaly (E) to True Anomaly (nu)
+ nu = 2 * np.arctan2(
+ np.sqrt(1 + e) * np.sin(E / 2),
+ np.sqrt(1 - e) * np.cos(E / 2)
+ )
+ return nu
+
+ def position_vector_in_orbital_plane(self):
+ """Compute position vector in the orbital plane at epoch.
+
+ The x-axis points toward the perigee.
+ """
+ true_anomaly = self._get_true_anomaly()
+
+ # Calculate radius (r) and coordinates using the True Anomaly
+ r = self.semi_major_axis * (1 - self.eccentricity**2) / \
+ (1 + self.eccentricity * np.cos(true_anomaly))
+
+ x = r * np.cos(true_anomaly)
+ y = r * np.sin(true_anomaly)
+
+ return np.array([x, y])
+
+ def _get_velocity_at_apsis(self, e_1, e_2):
+ """Helper method to compute orbital velocity at an apsis."""
+ mu = XKE**2 * AE**3
- self.period = np.pi * 2 / self.original_mean_motion
- self.perigee = (self.semi_major_axis * (1 - self.excentricity) / AE - AE) * XKMPER
- self.right_ascension_lon = (self.right_ascension
- - astronomy.gmst(self.epoch))
+ # r_apsis is the distance at the apsis (perigee or apogee)
+ r_apsis = self.semi_major_axis * e_1
- if self.right_ascension_lon > np.pi:
- self.right_ascension_lon -= 2 * np.pi
+ # The velocity formula: V = sqrt( mu * (2/r - 1/a) ).
+ # For apse lines (r = a * (1-e^2)/(1+e*cos(nu)), it simplifies to:
+ # V = sqrt( mu * (1 ± e) / r )
+ v_er_per_min = np.sqrt(mu * (e_2 / r_apsis))
+
+ # Conversion factor: (AE * XKMPER) converts ER -> km; / 60 converts min -> s
+ conversion_factor = (AE * XKMPER) / 60.0
+
+ return v_er_per_min * conversion_factor
+
+ def velocity_at_perigee(self):
+ """Compute orbital velocity at perigee in km/s."""
+ return self._get_velocity_at_apsis(1 - self.eccentricity, 1 + self.eccentricity)
+
+ def velocity_at_apogee(self):
+ """Compute orbital velocity at apogee in km/s."""
+ return self._get_velocity_at_apsis(1 + self.eccentricity, 1 - self.eccentricity)
def _calculate_mean_motion_and_semi_major_axis(self):
+ """Apply SGP4 perturbation corrections to mean motion and semi-major axis.
+
+ Based on inclination and eccentricity.
+ """
a_1 = (XKE / self.mean_motion) ** (2.0 / 3)
- delta_1 = ((3 / 2.0) * (CK2 / a_1**2) * ((3 * np.cos(self.inclination)**2 - 1) /
- (1 - self.excentricity**2)**(2.0 / 3)))
+ delta_1 = (3 / 2.0) * (CK2 / a_1**2) * ((3 * np.cos(self.inclination)**2 - 1) /
+ (1 - self.eccentricity**2)**(3 / 2))
a_0 = a_1 * (1 - delta_1 / 3 - delta_1**2 - (134.0 / 81) * delta_1**3)
- delta_0 = ((3 / 2.0) * (CK2 / a_0**2) * ((3 * np.cos(self.inclination)**2 - 1) /
- (1 - self.excentricity**2)**(2.0 / 3)))
+ delta_0 = (3 / 2.0) * (CK2 / a_0**2) * ((3 * np.cos(self.inclination)**2 - 1) /
+ (1 - self.eccentricity**2)**(3 / 2))
+
+ corrected_mean_motion = self.mean_motion / (1 + delta_0)
+ corrected_semi_major_axis = a_0 / (1 - delta_0)
- return (self.mean_motion / (1 + delta_0), a_0 / (1 - delta_0))
+ return corrected_mean_motion, corrected_semi_major_axis
class _SGDP4Base:
@@ -619,7 +710,7 @@ class _SGDP4Base:
_check_orbital_elements(orbit_elements)
- self.eo = orbit_elements.excentricity
+ self.eo = orbit_elements.eccentricity
self.xincl = orbit_elements.inclination
self.xno = orbit_elements.original_mean_motion
self.bstar = orbit_elements.bstar
@@ -1205,8 +1296,8 @@ class _Keplerians:
def _check_orbital_elements(orbit_elements):
- if not (0 < orbit_elements.excentricity < ECC_LIMIT_HIGH):
- raise OrbitalError("Eccentricity out of range: %e" % orbit_elements.excentricity)
+ if not (0 < orbit_elements.eccentricity < ECC_LIMIT_HIGH):
+ raise OrbitalError("Eccentricity out of range: %e" % orbit_elements.eccentricity)
if not ((0.0035 * 2 * np.pi / XMNPDA) < orbit_elements.original_mean_motion < (18 * 2 * np.pi / XMNPDA)):
raise OrbitalError("Mean motion out of range: %e" % orbit_elements.original_mean_motion)
if not (0 < orbit_elements.inclination < np.pi):
=====================================
pyorbital/tests/test_orbit_elements.py
=====================================
@@ -0,0 +1,296 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2012-2024 Pytroll Community
+
+# Author(s):
+
+# Martin Raspaud <martin.raspaud at smhi.se>
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+"""Test the orbit orbit elements."""
+
+import datetime
+
+import numpy as np
+import pytest
+
+from pyorbital.orbital import (
+ AE,
+ ECC_EPS,
+ ECC_LIMIT_HIGH,
+ XKMPER,
+ XMNPDA,
+ OrbitElements,
+)
+
+
+class MockTLE:
+ """Mock TLE object for testing OrbitElements."""
+
+ def __init__(self):
+ """Initialize mock TLE values for testing."""
+ self.epoch = datetime.datetime(2025, 9, 30, 12, 0, 0)
+ self.eccentricity = 0.001
+ self.inclination = 98.7
+ self.right_ascension = 120.0
+ self.arg_perigee = 87.0
+ self.mean_anomaly = 0.0
+ self.mean_motion = 14.2
+ self.mean_motion_derivative = 0.0
+ self.mean_motion_sec_derivative = 0.0
+ self.bstar = 0.0001
+
+
+def test_orbit_elements_computation():
+ """Test basic orbital element computations (SMA, Period, Angle Range)."""
+ tle = MockTLE()
+ orbit = OrbitElements(tle)
+
+ assert isinstance(orbit.semi_major_axis, float)
+ assert orbit.semi_major_axis > 0
+ assert orbit.period > 0
+ assert -np.pi <= orbit.right_ascension_lon <= np.pi
+
+
+def test_zero_eccentricity():
+ """Test perigee calculation with zero eccentricity."""
+ tle = MockTLE()
+ tle.eccentricity = 0.0
+ orbit = OrbitElements(tle)
+ # Perigee == Apogee altitude when e=0
+ assert orbit.perigee == pytest.approx(
+ (orbit.semi_major_axis / AE - AE) * XKMPER, abs=1e-3
+ )
+
+
+def test_retrograde_orbit_inclination():
+ """Test basic inclination check for retrograde orbit."""
+ tle = MockTLE()
+ tle.inclination = 120.0
+ orbit = OrbitElements(tle)
+ assert orbit.inclination > np.pi / 2
+
+
+def test_ra_lon_wrapping():
+ """Test longitude wrapping of right ascension with input > 360 deg."""
+ tle = MockTLE()
+ tle.right_ascension = 400.0 # degrees, > 360
+ orbit = OrbitElements(tle)
+ assert -np.pi <= orbit.right_ascension_lon <= np.pi
+
+
+def test_mean_motion_conversion():
+ """Test conversion of mean motion to radians per minute."""
+ tle = MockTLE()
+ orbit = OrbitElements(tle)
+ expected = tle.mean_motion * (2 * np.pi / XMNPDA)
+ assert orbit.mean_motion == pytest.approx(expected, rel=1e-6)
+
+
+ at pytest.mark.parametrize("incl_deg", [0, 45, 90, 135])
+def test_semi_major_axis_vs_inclination(incl_deg):
+ """Test semi-major axis computation across inclinations."""
+ tle = MockTLE()
+ tle.inclination = incl_deg
+ orbit = OrbitElements(tle)
+ assert orbit.semi_major_axis > 0
+
+
+ at pytest.mark.parametrize("bstar", [0.0, 1e-5, 0.0001, 0.01])
+def test_bstar_scaling(bstar):
+ """Test scaling of bstar drag term."""
+ tle = MockTLE()
+ tle.bstar = bstar
+ orbit = OrbitElements(tle)
+ assert orbit.bstar == pytest.approx(bstar * AE)
+
+
+def test_mean_motion_derivatives():
+ """Test mean motion derivatives are correctly computed."""
+ tle = MockTLE()
+ orbit = OrbitElements(tle)
+ tle.mean_motion_derivative = 1e-5
+ tle.mean_motion_sec_derivative = 1e-7
+ orbit = OrbitElements(tle)
+ assert orbit.mean_motion_derivative > 0
+ assert orbit.mean_motion_sec_derivative > 0
+
+
+def test_apogee_computation():
+ """Test apogee altitude calculation."""
+ tle = MockTLE()
+ orbit = OrbitElements(tle)
+ expected_apogee = (
+ (orbit.semi_major_axis * (1 + orbit.eccentricity)) / AE - AE
+ ) * XKMPER
+ assert orbit.apogee == pytest.approx(expected_apogee, abs=1e-3)
+
+
+def test_semi_major_axis_accuracy():
+ """Test semi-major axis against analytical reference value (in km)."""
+ tle = MockTLE()
+ orbit = OrbitElements(tle)
+ expected_sma_km = 7200.645659667062
+ computed_sma_km = orbit.semi_major_axis * XKMPER / AE
+ assert computed_sma_km == pytest.approx(expected_sma_km, abs=0.1)
+
+
+def test_orbital_period_accuracy():
+ """Test orbital period against corrected mean motion (in seconds)."""
+ tle = MockTLE()
+ orbit = OrbitElements(tle)
+ expected_period_sec = 6080.901176943267
+ computed_period_sec = orbit.period * 60
+ assert computed_period_sec == pytest.approx(expected_period_sec, abs=1.0)
+
+
+def test_velocity_at_perigee_accuracy():
+ """Test orbital velocity at perigee against analytical reference."""
+ tle = MockTLE()
+ orbit = OrbitElements(tle)
+ expected_velocity_kms = 7.44762253625217
+ computed_velocity_kms = orbit.velocity_at_perigee()
+ assert computed_velocity_kms == pytest.approx(expected_velocity_kms, abs=0.005)
+
+
+def test_velocity_at_apogee_accuracy():
+ """Test orbital velocity at apogee against analytical reference."""
+ tle = MockTLE()
+ orbit = OrbitElements(tle)
+ expected_velocity_kms = 7.432742171544375
+ computed_velocity_kms = orbit.velocity_at_apogee()
+ assert computed_velocity_kms == pytest.approx(expected_velocity_kms, abs=0.005)
+
+
+def test_position_vector_in_orbital_plane_perigee_accuracy():
+ """Test position vector at perigee (Mean Anomaly = 0°)."""
+ tle = MockTLE()
+ tle.eccentricity = 0.001
+ tle.mean_anomaly = 0.0
+ orbit = OrbitElements(tle)
+ pos = orbit.position_vector_in_orbital_plane()
+ expected_x = 1.1278289051591721 # Earth radii
+ expected_y = 0.0
+ assert pos[0] == pytest.approx(expected_x, rel=1e-6)
+ assert pos[1] == pytest.approx(expected_y, abs=1e-8)
+
+
+ at pytest.mark.parametrize(
+ ("mean_anomaly_deg", "expected_radius"),
+ [
+ (0, 7193.445014007397),
+ (90, 7200.6528603079205),
+ (180, 7207.84630532673),
+ (270, 7200.6528603079205),
+ ],
+)
+def test_position_vector_in_orbital_plane_varied(mean_anomaly_deg, expected_radius):
+ """Verify position vector magnitude matches expected radius at given mean anomaly."""
+ tle = MockTLE()
+ tle.mean_anomaly = mean_anomaly_deg
+ orbit = OrbitElements(tle)
+ pos = orbit.position_vector_in_orbital_plane()
+ actual_radius = np.linalg.norm(pos) * XKMPER
+ assert np.isclose(actual_radius, expected_radius, rtol=1e-6)
+
+
+ at pytest.mark.parametrize(
+ ("eccentricity", "expected"),
+ [
+ (0.0005, True),
+ (0.01, False),
+ ],
+)
+def test_is_circular_property(eccentricity, expected):
+ """Test circular orbit detection based on eccentricity."""
+ tle = MockTLE()
+ tle.eccentricity = eccentricity
+ orbit = OrbitElements(tle)
+ assert orbit.is_circular == expected
+
+
+ at pytest.mark.parametrize(
+ ("inclination_deg", "expected"),
+ [
+ (100.0, True),
+ (80.0, False),
+ ],
+)
+def test_is_retrograde_property(inclination_deg, expected):
+ """Test retrograde orbit detection based on inclination."""
+ tle = MockTLE()
+ tle.inclination = inclination_deg
+ orbit = OrbitElements(tle)
+ assert orbit.is_retrograde == expected
+
+
+ at pytest.mark.parametrize("eccentricity", [0.001, 0.01, 0.1, 0.5])
+def test_velocity_perigee_greater_than_apogee(eccentricity):
+ """Ensure velocity at perigee is greater than at apogee for elliptical orbits."""
+ tle = MockTLE()
+ tle.eccentricity = eccentricity
+ orbit = OrbitElements(tle)
+ v_perigee = orbit.velocity_at_perigee()
+ v_apogee = orbit.velocity_at_apogee()
+ assert v_perigee > v_apogee
+
+
+ at pytest.mark.parametrize("eccentricity", [0.0, ECC_EPS / 10, ECC_EPS])
+def test_velocity_equal_for_circular_orbits(eccentricity):
+ """Ensure velocity at perigee equals velocity at apogee for nearly circular orbits."""
+ tle = MockTLE()
+ tle.eccentricity = eccentricity
+ orbit = OrbitElements(tle)
+ v_perigee = orbit.velocity_at_perigee()
+ v_apogee = orbit.velocity_at_apogee()
+ assert v_perigee == pytest.approx(v_apogee, rel=1e-5)
+
+
+def test_high_eccentricity_limit():
+ """Test behavior near the upper eccentricity limit."""
+ tle = MockTLE()
+ tle.eccentricity = ECC_LIMIT_HIGH
+ orbit = OrbitElements(tle)
+ assert orbit.semi_major_axis > 0
+ assert orbit.velocity_at_perigee() > orbit.velocity_at_apogee()
+
+
+def test_sgp4_unnormalization_value():
+ """Validate SGP4 un-normalization of mean motion and semi-major axis.
+
+ This test uses hardcoded reference values from validated SGP4 output.
+ """
+
+ class RefTLE(MockTLE):
+ """Reference TLE with fixed orbital parameters for precision testing."""
+
+ def __init__(self):
+ super().__init__()
+ """Initialize reference TLE values for SGP4 unnormalization test."""
+ self.inclination = 98.7408 # degrees
+ self.eccentricity = 0.001140
+ self.mean_motion = 14.28634842 # rev/day
+ self.epoch = datetime.datetime(2200, 1, 1, 0, 0, 0)
+
+ tle = RefTLE()
+ orbit = OrbitElements(tle)
+
+ # Hardcoded expected values from validated SGP4 propagation
+ expected_mean_motion = 0.06237319306246916 # rad/min
+ expected_semi_major_axis = 1.1244009310620886 # Earth radii
+
+ assert orbit.original_mean_motion == pytest.approx(expected_mean_motion, rel=1e-8)
+ assert orbit.semi_major_axis == pytest.approx(expected_semi_major_axis, rel=1e-8)
=====================================
pyorbital/tests/test_tlefile.py
=====================================
@@ -32,6 +32,8 @@ from unittest import mock
import pytest
+from pyorbital.config import config
+
LINE0 = "ISS (ZARYA)"
LINE1 = "1 25544U 98067A 08264.51782528 -.00002182 00000-0 -11606-4 0 2927"
LINE2 = "2 25544 51.6416 247.4627 0006703 130.5360 325.0288 15.72125391563537"
@@ -120,9 +122,9 @@ def test_read_tlefile_standard_platform_name(monkeypatch, fake_platforms_txt_fil
from pyorbital import tlefile
path_to_platforms_txt_file = fake_platforms_txt_file.parent
- monkeypatch.setenv("PYORBITAL_CONFIG_PATH", str(path_to_platforms_txt_file))
- tle_n21 = tlefile.read("NOAA-21", str(fake_tlefile))
+ with config.set(config_path=str(path_to_platforms_txt_file)):
+ tle_n21 = tlefile.read("NOAA-21", str(fake_tlefile))
assert tle_n21.line1 == "1 54234U 22150A 23045.56664999 .00000332 00000+0 17829-3 0 9993"
assert tle_n21.line2 == "2 54234 98.7059 345.5113 0001226 81.6523 278.4792 14.19543871 13653"
@@ -135,9 +137,9 @@ def test_read_tlefile_non_standard_platform_name(monkeypatch, fake_platforms_txt
from pyorbital import tlefile
path_to_platforms_txt_file = fake_platforms_txt_file.parent
- monkeypatch.setenv("PYORBITAL_CONFIG_PATH", str(path_to_platforms_txt_file))
- tle_n20 = tlefile.read("NOAA 20", str(fake_tlefile))
+ with config.set(config_path=str(path_to_platforms_txt_file)):
+ tle_n20 = tlefile.read("NOAA 20", str(fake_tlefile))
assert tle_n20.line1 == "1 43013U 17073A 23045.54907786 .00000253 00000+0 14081-3 0 9995"
assert tle_n20.line2 == "2 43013 98.7419 345.5839 0001610 80.3742 279.7616 14.19558274271576"
@@ -168,11 +170,11 @@ def test_read_tlefile_non_standard_platform_name_matching_start_of_name_in_tlefi
from pyorbital import tlefile
path_to_platforms_txt_file = fake_platforms_txt_file.parent
- monkeypatch.setenv("PYORBITAL_CONFIG_PATH", str(path_to_platforms_txt_file))
- with pytest.raises(KeyError) as exc_info:
- with caplog.at_level(logging.DEBUG):
- _ = tlefile.read(sat_name, str(fake_tlefile))
+ with config.set(config_path=str(path_to_platforms_txt_file)):
+ with pytest.raises(KeyError) as exc_info:
+ with caplog.at_level(logging.DEBUG):
+ _ = tlefile.read(sat_name, str(fake_tlefile))
assert f"Found a possible match: {expected}?" in caplog.text
assert str(exc_info.value) == f'"Found no TLE entry for \'{sat_name}\'"'
@@ -308,11 +310,10 @@ def test_get_config_path_ppp_config_set_and_pyorbital(caplog, monkeypatch):
from pyorbital.tlefile import _get_config_path
pyorbital_config_dir = "/path/to/pyorbital/config/dir"
- monkeypatch.setenv("PYORBITAL_CONFIG_PATH", pyorbital_config_dir)
monkeypatch.setenv("PPP_CONFIG_DIR", "/path/to/old/mpop/config/dir")
-
- with caplog.at_level(logging.WARNING):
- res = _get_config_path()
+ with config.set(config_path=pyorbital_config_dir):
+ with caplog.at_level(logging.WARNING):
+ res = _get_config_path()
assert res == pyorbital_config_dir
assert caplog.text == ""
@@ -327,10 +328,10 @@ def test_get_config_path_pyorbital_ppp_missing(caplog, monkeypatch):
from pyorbital.tlefile import _get_config_path
pyorbital_config_dir = "/path/to/pyorbital/config/dir"
- monkeypatch.setenv("PYORBITAL_CONFIG_PATH", pyorbital_config_dir)
- with caplog.at_level(logging.DEBUG):
- res = _get_config_path()
+ with config.set(config_path=pyorbital_config_dir):
+ with caplog.at_level(logging.DEBUG):
+ res = _get_config_path()
assert res == pyorbital_config_dir
log_output = ("Path to the Pyorbital configuration (where e.g. " +
@@ -384,6 +385,25 @@ def test_get_uris_and_open_func_using_tles_env(caplog, fake_local_tles_dir, monk
assert log_message in caplog.text
+def test_get_uris_and_open_func_using_empty_tles_env(caplog, fake_local_tles_dir, monkeypatch):
+ """Test getting the uris and associated open-function for reading tles.
+
+ Test providing no tle file but using TLES env that returns no files.
+ """
+ from pyorbital.tlefile import _get_uris_and_open_func
+
+ monkeypatch.setenv("TLES", "/no/files/here/tle*txt")
+
+ with caplog.at_level(logging.DEBUG):
+ with pytest.warns():
+ uris, _ = _get_uris_and_open_func()
+
+ warning_text = "TLES environment variable points to no TLE files"
+ debug_text = "Fetch TLE from the internet."
+ assert warning_text in caplog.text
+ assert debug_text in caplog.text
+
+
class TLETest(unittest.TestCase):
"""Test TLE reading.
@@ -417,7 +437,7 @@ class TLETest(unittest.TestCase):
# line 2
assert tle.inclination == 51.6416
assert tle.right_ascension == 247.4627
- assert tle.excentricity == 0.0006703
+ assert tle.eccentricity == 0.0006703
assert tle.arg_perigee == 130.536
assert tle.mean_anomaly == 325.0288
assert tle.mean_motion == 15.72125391
@@ -513,11 +533,7 @@ class TestDownloader(unittest.TestCase):
@mock.patch("pyorbital.tlefile.requests")
def test_fetch_plain_tle_not_configured(self, requests):
- """Test downloading and a TLE file from internet."""
- requests.get = mock.MagicMock()
- requests.get.return_value = _get_req_response(200)
-
- # Not configured
+ """Test that plain TLE downloading is not called when not configured."""
self.dl.config["downloaders"] = {}
res = self.dl.fetch_plain_tle()
assert res == {}
@@ -525,7 +541,7 @@ class TestDownloader(unittest.TestCase):
@mock.patch("pyorbital.tlefile.requests")
def test_fetch_plain_tle_two_sources(self, requests):
- """Test downloading and a TLE file from internet."""
+ """Test downloading a TLE file from internet."""
requests.get = mock.MagicMock()
requests.get.return_value = _get_req_response(200)
@@ -540,7 +556,24 @@ class TestDownloader(unittest.TestCase):
assert "source_2" in res
assert len(res["source_2"]) == 1
assert mock.call("mocked_url_1", timeout=15) in requests.get.mock_calls
- assert len(requests.get.mock_calls) == 4
+ assert len([c for c in requests.get.mock_calls if c.args]) == 4
+
+ @mock.patch("logging.error")
+ @mock.patch("pyorbital.tlefile.requests.get")
+ def test_fetch_plain_tle_timeout(self, requests_get, logging_error):
+ """Test that timeout is logged."""
+ from requests.exceptions import Timeout
+
+ requests_get.side_effect = Timeout
+
+ self.dl.config["downloaders"] = FETCH_PLAIN_TLE_CONFIG
+
+ res = self.dl.fetch_plain_tle()
+ for url in ["mocked_url_1", "mocked_url_2", "mocked_url_3", "mocked_url_4"]:
+ expected = mock.call(f"Failed to make request to {url} within 15 seconds!")
+ assert expected in logging_error.mock_calls
+ assert not res["source_1"]
+ assert not res["source_2"]
@mock.patch("pyorbital.tlefile.requests")
def test_fetch_plain_tle_server_is_a_teapot(self, requests):
@@ -560,7 +593,7 @@ class TestDownloader(unittest.TestCase):
assert len(res["source_2"]) == 0
assert mock.call("mocked_url_1", timeout=15) in requests.get.mock_calls
- assert len(requests.get.mock_calls) == 4
+ assert len([c for c in requests.get.mock_calls if c.args]) == 4
@mock.patch("pyorbital.tlefile.requests")
def test_fetch_spacetrack_login_fails(self, requests):
@@ -721,6 +754,7 @@ class TestSQLiteTLE(unittest.TestCase):
"""Clean temporary files."""
with suppress(PermissionError, NotADirectoryError):
self.temp_dir.cleanup()
+ self.db.close()
def test_init(self):
"""Test that the init did what it should have."""
@@ -821,7 +855,9 @@ class TestSQLiteTLE(unittest.TestCase):
assert "%" not in files[0]
# The satellite name should be in the file
with open(files[0], "r") as fid:
- data = fid.read().split("\n")
+ data = fid.read()
+ assert data.endswith("\n")
+ data = data.strip("\n").split("\n")
assert len(data) == 3
assert "ISS" in data[0]
assert data[1] == LINE1
@@ -845,7 +881,7 @@ class TestSQLiteTLE(unittest.TestCase):
files = sorted(glob.glob(os.path.join(tle_dir, "tle_*txt")))
assert len(files) == 2
with open(files[1], "r") as fid:
- data = fid.read().split("\n")
+ data = fid.read().strip("\n").split("\n")
assert len(data) == 2
assert data[0] == LINE1
assert data[1] == LINE2
@@ -856,6 +892,6 @@ def test_tle_instance_printing():
tle = Tle("ISS", line1=LINE1, line2=LINE2)
- expected = "{'arg_perigee': 130.536,\n 'bstar': -1.1606e-05,\n 'classification': 'U',\n 'element_number': 292,\n 'ephemeris_type': 0,\n 'epoch': np.datetime64('2008-09-20T12:25:40.104192'),\n 'epoch_day': 264.51782528,\n 'epoch_year': '08',\n 'excentricity': 0.0006703,\n 'id_launch_number': '067',\n 'id_launch_piece': 'A ',\n 'id_launch_year': '98',\n 'inclination': 51.6416,\n 'mean_anomaly': 325.0288,\n 'mean_motion': 15.72125391,\n 'mean_motion_derivative': -2.182e-05,\n 'mean_motion_sec_derivative': 0.0,\n 'orbit': 56353,\n 'right_ascension': 247.4627,\n 'satnumber': '25544'}" # noqa
+ expected = "{'arg_perigee': 130.536,\n 'bstar': -1.1606e-05,\n 'classification': 'U',\n 'eccentricity': 0.0006703,\n 'element_number': 292,\n 'ephemeris_type': 0,\n 'epoch': np.datetime64('2008-09-20T12:25:40.104192'),\n 'epoch_day': 264.51782528,\n 'epoch_year': '08',\n 'id_launch_number': '067',\n 'id_launch_piece': 'A ',\n 'id_launch_year': '98',\n 'inclination': 51.6416,\n 'mean_anomaly': 325.0288,\n 'mean_motion': 15.72125391,\n 'mean_motion_derivative': -2.182e-05,\n 'mean_motion_sec_derivative': 0.0,\n 'orbit': 56353,\n 'right_ascension': 247.4627,\n 'satnumber': '25544'}" # noqa
assert str(tle) == expected
=====================================
pyorbital/tlefile.py
=====================================
@@ -24,13 +24,17 @@ import io
import logging
import os
import sqlite3
+import warnings
from itertools import zip_longest
from urllib.request import urlopen
+from warnings import warn
import defusedxml.ElementTree as ET
import numpy as np
import requests
+from pyorbital.config import config
+
TLE_GROUPS = ("active",
"weather",
"resource",
@@ -48,20 +52,17 @@ TLE_URLS = [f"https://celestrak.org/NORAD/elements/gp.php?GROUP={group}&FORMAT=t
LOGGER = logging.getLogger(__name__)
PKG_CONFIG_DIR = os.path.join(os.path.realpath(os.path.dirname(__file__)), "etc")
-class TleDownloadTimeoutError(Exception):
- """TLE download timeout exception."""
-
def _get_config_path():
"""Get the config path for Pyorbital."""
- if "PPP_CONFIG_DIR" in os.environ and "PYORBITAL_CONFIG_PATH" not in os.environ:
+ if "PPP_CONFIG_DIR" in os.environ and "config_path" not in config:
LOGGER.warning(
"The use of PPP_CONFIG_DIR is no longer supported!" +
" Please use PYORBITAL_CONFIG_PATH if you need a custom config path for pyorbital!")
LOGGER.debug("Using the package default for configuration: %s", PKG_CONFIG_DIR)
return PKG_CONFIG_DIR
else:
- pyorbital_config_path = os.getenv("PYORBITAL_CONFIG_PATH", PKG_CONFIG_DIR)
+ pyorbital_config_path = config.get("config_path", PKG_CONFIG_DIR)
LOGGER.debug("Path to the Pyorbital configuration (where e.g. platforms.txt is found): %s",
str(pyorbital_config_path))
@@ -161,7 +162,7 @@ class ChecksumError(Exception):
"""ChecksumError."""
-class Tle(object):
+class Tle:
"""Class holding TLE objects."""
def __init__(self, platform, tle_file=None, line1=None, line2=None):
@@ -186,7 +187,7 @@ class Tle(object):
self.element_number = None
self.inclination = None
self.right_ascension = None
- self.excentricity = None
+ self.eccentricity = None
self.arg_perigee = None
self.mean_anomaly = None
self.mean_motion = None
@@ -196,6 +197,12 @@ class Tle(object):
self._checksum()
self._parse_tle()
+ @property
+ def excentricity(self):
+ """Get 'eccentricity' using legacy 'excentricity' name."""
+ warnings.warn("The 'eccentricity' property is deprecated in favor of 'eccentricity'", stacklevel=2)
+ return self.eccentricity
+
@property
def line1(self):
"""Return first TLE line."""
@@ -271,12 +278,40 @@ class Tle(object):
self.inclination = float(self._line2[8:16])
self.right_ascension = float(self._line2[17:25])
- self.excentricity = int(self._line2[26:33]) * 10 ** -7
+ self.eccentricity = int(self._line2[26:33]) * 10 ** -7
self.arg_perigee = float(self._line2[34:42])
self.mean_anomaly = float(self._line2[43:51])
self.mean_motion = float(self._line2[52:63])
self.orbit = int(self._line2[63:68])
+ def to_dict(self):
+ """Return the raw, parsed TLE elements as a dictionary."""
+ return {
+ "platform": self.platform,
+ "satnumber": self.satnumber,
+ "classification": self.classification,
+ "id_launch_year": self.id_launch_year,
+ "id_launch_number": self.id_launch_number,
+ "id_launch_piece": self.id_launch_piece,
+ "epoch_year": self.epoch_year,
+ "epoch_day": self.epoch_day,
+ "epoch": self.epoch,
+ "mean_motion_derivative": self.mean_motion_derivative,
+ "mean_motion_sec_derivative": self.mean_motion_sec_derivative,
+ "bstar": self.bstar,
+ "ephemeris_type": self.ephemeris_type,
+ "element_number": self.element_number,
+ "inclination": self.inclination,
+ "right_ascension": self.right_ascension,
+ "eccentricity": self.eccentricity,
+ "arg_perigee": self.arg_perigee,
+ "mean_anomaly": self.mean_anomaly,
+ "mean_motion": self.mean_motion,
+ "orbit": self.orbit,
+ "line1": self._line1,
+ "line2": self._line2,
+ }
+
def __str__(self):
"""Format the class data for printing."""
import pprint
@@ -294,36 +329,62 @@ def _get_local_tle_path_from_env():
def _get_uris_and_open_func(tle_file=None):
"""Get the uri's and the adequate file open call for the TLE files."""
- def _open(filename):
- return io.open(filename, "rb")
-
local_tle_path = _get_local_tle_path_from_env()
if tle_file:
- if isinstance(tle_file, io.StringIO):
- uris = (tle_file,)
- open_func = _dummy_open_stringio
- elif "ADMIN_MESSAGE" in tle_file:
- uris = (io.StringIO(read_tle_from_mmam_xml_file(tle_file)),)
- open_func = _dummy_open_stringio
- else:
- uris = (tle_file,)
- open_func = _open
+ uris, open_func = _get_tle_file_uris_and_open_method(tle_file)
elif local_tle_path:
- # TODO: get the TLE file closest in time to the actual satellite
- # overpass, NOT the latest!
- list_of_tle_files = glob.glob(local_tle_path)
+ uris, open_func = _get_local_uris_and_open_method(local_tle_path)
+ else:
+ uris, open_func = _get_internet_uris_and_open_method()
+ return uris, open_func
+
+
+def _get_tle_file_uris_and_open_method(tle_file):
+ if isinstance(tle_file, io.StringIO):
+ uris = (tle_file,)
+ open_func = _dummy_open_stringio
+ elif "ADMIN_MESSAGE" in tle_file:
+ uris = (io.StringIO(read_tle_from_mmam_xml_file(tle_file)),)
+ open_func = _dummy_open_stringio
+ else:
+ uris = (tle_file,)
+ open_func = _open
+ return uris, open_func
+
+
+def _open(filename):
+ return io.open(filename, "rb")
+
+
+def _get_local_uris_and_open_method(local_tle_path):
+ # TODO: get the TLE file closest in time to the actual satellite
+ # overpass, NOT the latest!
+ list_of_tle_files = glob.glob(local_tle_path)
+ if list_of_tle_files:
uris = (max(list_of_tle_files, key=os.path.getctime), )
LOGGER.debug("Reading TLE from %s", uris[0])
open_func = _open
else:
- LOGGER.debug("Fetch TLE from the internet.")
- uris = TLE_URLS
- open_func = urlopen
+ if config.get("fetch_from_celestrak", None) is not True:
+ warn("In the future, implicit downloads of TLEs from Celestrak will be disabled by default. "
+ "You can enable it (and remove this warning) by setting PYORBITAL_FETCH_FROM_CELESTRAK to True.",
+ DeprecationWarning)
+ LOGGER.warning("TLES environment variable points to no TLE files")
+ throttle_warning = "TLEs will be downloaded from Celestrak, which can throttle the connection."
+ LOGGER.warning(throttle_warning)
+ warnings.warn(throttle_warning)
+
+ uris, open_func = _get_internet_uris_and_open_method()
return uris, open_func
+def _get_internet_uris_and_open_method():
+ LOGGER.debug("Fetch TLE from the internet.")
+ return TLE_URLS, urlopen
+
+
def _get_first_tle(uris, open_func, platform=""):
return _get_tles_from_uris(uris, open_func, platform=platform, only_first=True)
@@ -421,8 +482,9 @@ class Downloader(object):
try:
req = requests.get(uri, timeout=15) # 15 seconds
except requests.exceptions.Timeout:
- raise TleDownloadTimeoutError(f"Failed to make request to {str(uri)} within 15 seconds!")
- if req.status_code == 200:
+ logging.error(f"Failed to make request to {str(uri)} within 15 seconds!")
+ req = None
+ if req and req.status_code == 200:
tles[source] += _parse_tles_for_downloader((req.text,), io.StringIO)
else:
failures.append(uri)
@@ -430,8 +492,7 @@ class Downloader(object):
logging.error(
"Could not fetch TLEs from %s, %d failure(s): [%s]",
source, len(failures), ", ".join(failures))
- logging.info("Downloaded %d TLEs from %s",
- len(tles[source]), source)
+ logging.info("Downloaded %d TLEs from %s", len(tles[source]), source)
return tles
def fetch_spacetrack(self):
@@ -619,6 +680,8 @@ class SQLiteTLE(object):
with open(fname, "w") as fid:
fid.write("\n".join(data))
+ # Add a line-change after the last entry
+ fid.write("\n")
logging.info("Wrote %d TLEs to %s", len(data), fname)
=====================================
pyproject.toml
=====================================
@@ -10,6 +10,7 @@ dependencies = ["numpy>=1.19.0",
"requests",
"defusedxml",
"pyproj>=3.7.1",
+ "donfig",
]
readme = "README.md"
requires-python = ">=3.10"
View it on GitLab: https://salsa.debian.org/debian-gis-team/pyorbital/-/compare/c5f6b6d8c862ff3a958e95a5e5f5c2e5fb5b464f...5ecfeb41df96fb28420283b67bbd5294e3d41fe8
--
View it on GitLab: https://salsa.debian.org/debian-gis-team/pyorbital/-/compare/c5f6b6d8c862ff3a958e95a5e5f5c2e5fb5b464f...5ecfeb41df96fb28420283b67bbd5294e3d41fe8
You're receiving this email because of your account on salsa.debian.org.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/pkg-grass-devel/attachments/20251122/25ab5c1c/attachment-0001.htm>
More information about the Pkg-grass-devel
mailing list