fastgpx 0.2.2__tar.gz → 0.4.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.
Files changed (62) hide show
  1. {fastgpx-0.2.2 → fastgpx-0.4.0}/PKG-INFO +1 -1
  2. {fastgpx-0.2.2 → fastgpx-0.4.0}/docs/source/conf.py +43 -0
  3. {fastgpx-0.2.2 → fastgpx-0.4.0}/pyproject.toml +1 -1
  4. {fastgpx-0.2.2 → fastgpx-0.4.0}/src/cpp/fastgpx/fastgpx.cpp +15 -1
  5. {fastgpx-0.2.2 → fastgpx-0.4.0}/src/cpp/fastgpx/fastgpx.hpp +3 -0
  6. {fastgpx-0.2.2 → fastgpx-0.4.0}/src/cpp/fastgpx/fastgpx_test.cpp +6 -0
  7. {fastgpx-0.2.2 → fastgpx-0.4.0}/src/cpp/python_fastgpx.cpp +8 -1
  8. {fastgpx-0.2.2 → fastgpx-0.4.0}/src/cpp/python_utc_chrono.hpp +13 -1
  9. {fastgpx-0.2.2 → fastgpx-0.4.0}/src/fastgpx/__init__.pyi +1 -1
  10. {fastgpx-0.2.2 → fastgpx-0.4.0}/src/fastgpx/fastgpx/__init__.pyi +9 -0
  11. {fastgpx-0.2.2 → fastgpx-0.4.0}/tests/test_fastgpx.py +19 -1
  12. fastgpx-0.4.0/tests/test_time_bounds.py +73 -0
  13. {fastgpx-0.2.2 → fastgpx-0.4.0}/uv.lock +1 -1
  14. {fastgpx-0.2.2 → fastgpx-0.4.0}/.clang-format +0 -0
  15. {fastgpx-0.2.2 → fastgpx-0.4.0}/.gitignore +0 -0
  16. {fastgpx-0.2.2 → fastgpx-0.4.0}/.python-version +0 -0
  17. {fastgpx-0.2.2 → fastgpx-0.4.0}/CMakeLists.txt +0 -0
  18. {fastgpx-0.2.2 → fastgpx-0.4.0}/CMakeSettings.json +0 -0
  19. {fastgpx-0.2.2 → fastgpx-0.4.0}/Development.md +0 -0
  20. {fastgpx-0.2.2 → fastgpx-0.4.0}/GPX.md +0 -0
  21. {fastgpx-0.2.2 → fastgpx-0.4.0}/LICENSE.md +0 -0
  22. {fastgpx-0.2.2 → fastgpx-0.4.0}/README.md +0 -0
  23. {fastgpx-0.2.2 → fastgpx-0.4.0}/benchmarks/benchmark_gpx.py +0 -0
  24. {fastgpx-0.2.2 → fastgpx-0.4.0}/benchmarks/benchmark_polyline.py +0 -0
  25. {fastgpx-0.2.2 → fastgpx-0.4.0}/benchmarks/gpx_parse.md +0 -0
  26. {fastgpx-0.2.2 → fastgpx-0.4.0}/catch2.py +0 -0
  27. {fastgpx-0.2.2 → fastgpx-0.4.0}/coverage.bat +0 -0
  28. {fastgpx-0.2.2 → fastgpx-0.4.0}/datetime.md +0 -0
  29. {fastgpx-0.2.2 → fastgpx-0.4.0}/docs/Makefile +0 -0
  30. {fastgpx-0.2.2 → fastgpx-0.4.0}/docs/make.bat +0 -0
  31. {fastgpx-0.2.2 → fastgpx-0.4.0}/docs/source/api.rst +0 -0
  32. {fastgpx-0.2.2 → fastgpx-0.4.0}/docs/source/index.rst +0 -0
  33. {fastgpx-0.2.2 → fastgpx-0.4.0}/docs/source/overview.rst +0 -0
  34. {fastgpx-0.2.2 → fastgpx-0.4.0}/profiling/profile_polyline.py +0 -0
  35. {fastgpx-0.2.2 → fastgpx-0.4.0}/pytest.ini +0 -0
  36. {fastgpx-0.2.2 → fastgpx-0.4.0}/src/cpp/CMakeLists.txt +0 -0
  37. {fastgpx-0.2.2 → fastgpx-0.4.0}/src/cpp/app.cpp +0 -0
  38. {fastgpx-0.2.2 → fastgpx-0.4.0}/src/cpp/expected_gpx_data.json +0 -0
  39. {fastgpx-0.2.2 → fastgpx-0.4.0}/src/cpp/fastgpx/datetime.cpp +0 -0
  40. {fastgpx-0.2.2 → fastgpx-0.4.0}/src/cpp/fastgpx/datetime.hpp +0 -0
  41. {fastgpx-0.2.2 → fastgpx-0.4.0}/src/cpp/fastgpx/datetime_test.cpp +0 -0
  42. {fastgpx-0.2.2 → fastgpx-0.4.0}/src/cpp/fastgpx/errors.cpp +0 -0
  43. {fastgpx-0.2.2 → fastgpx-0.4.0}/src/cpp/fastgpx/errors.hpp +0 -0
  44. {fastgpx-0.2.2 → fastgpx-0.4.0}/src/cpp/fastgpx/errors_test.cpp +0 -0
  45. {fastgpx-0.2.2 → fastgpx-0.4.0}/src/cpp/fastgpx/filesystem.cpp +0 -0
  46. {fastgpx-0.2.2 → fastgpx-0.4.0}/src/cpp/fastgpx/filesystem.hpp +0 -0
  47. {fastgpx-0.2.2 → fastgpx-0.4.0}/src/cpp/fastgpx/filesystem_test.cpp +0 -0
  48. {fastgpx-0.2.2 → fastgpx-0.4.0}/src/cpp/fastgpx/geom.cpp +0 -0
  49. {fastgpx-0.2.2 → fastgpx-0.4.0}/src/cpp/fastgpx/geom.hpp +0 -0
  50. {fastgpx-0.2.2 → fastgpx-0.4.0}/src/cpp/fastgpx/geom_test.cpp +0 -0
  51. {fastgpx-0.2.2 → fastgpx-0.4.0}/src/cpp/fastgpx/polyline.cpp +0 -0
  52. {fastgpx-0.2.2 → fastgpx-0.4.0}/src/cpp/fastgpx/polyline.hpp +0 -0
  53. {fastgpx-0.2.2 → fastgpx-0.4.0}/src/cpp/fastgpx/test_data.cpp +0 -0
  54. {fastgpx-0.2.2 → fastgpx-0.4.0}/src/cpp/fastgpx/test_data.hpp +0 -0
  55. {fastgpx-0.2.2 → fastgpx-0.4.0}/src/cpp/fastgpx/test_data.json +0 -0
  56. {fastgpx-0.2.2 → fastgpx-0.4.0}/src/cpp/fastgpx/test_data_test.cpp +0 -0
  57. {fastgpx-0.2.2 → fastgpx-0.4.0}/src/fastgpx/__init__.py +0 -0
  58. {fastgpx-0.2.2 → fastgpx-0.4.0}/src/fastgpx/fastgpx/geo.pyi +0 -0
  59. {fastgpx-0.2.2 → fastgpx-0.4.0}/src/fastgpx/fastgpx/polyline.pyi +0 -0
  60. {fastgpx-0.2.2 → fastgpx-0.4.0}/src/fastgpx/py.typed +0 -0
  61. {fastgpx-0.2.2 → fastgpx-0.4.0}/tests/test_bounds.py +0 -0
  62. {fastgpx-0.2.2 → fastgpx-0.4.0}/tests/test_polyline.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastgpx
3
- Version: 0.2.2
3
+ Version: 0.4.0
4
4
  Summary: An experimental Python library for parsing GPX files fast.
5
5
  Keywords: gpx,parser,fast
6
6
  Author: Thomas Thomassen
@@ -3,9 +3,12 @@
3
3
  # For the full list of built-in configuration values, see the documentation:
4
4
  # https://www.sphinx-doc.org/en/master/usage/configuration.html
5
5
 
6
+ import re
6
7
  import os
7
8
  import sys
8
9
  import tomllib
10
+ import logging as std_logging
11
+ from sphinx.util import logging
9
12
 
10
13
  PROJECT_ROOT = os.path.abspath('../../')
11
14
 
@@ -49,3 +52,43 @@ autosummary_generate = True
49
52
 
50
53
  html_theme = 'sphinx_rtd_theme'
51
54
  html_static_path = ['_static']
55
+
56
+
57
+ # -- Adjust formatting for overloaded methods --------------------------------
58
+
59
+ logger = logging.getLogger(__name__)
60
+
61
+
62
+ def format_overloaded_docstring(app, what, name, obj, options, lines):
63
+ logger.info(f"[fastgpx-doc] Processing docstring for {name}")
64
+ if not lines:
65
+ return
66
+
67
+ logger.info(f"[fastgpx-doc] First line: {lines[0]}")
68
+ if lines[0].strip().startswith("Overloaded function"):
69
+ logger.info(f"[fastgpx-doc] Detected overloaded function in {name}")
70
+
71
+ new_lines = [".. rubric:: Overloads", ""]
72
+
73
+ for line in lines[1:]:
74
+ match = re.match(r"^\d+\.\s+(.*)", line.strip())
75
+ if match:
76
+ new_lines.extend([
77
+ ".. code-block:: python",
78
+ "",
79
+ f" {match.group(1)}",
80
+ ""
81
+ ])
82
+
83
+ lines.clear()
84
+ lines.extend(new_lines)
85
+
86
+
87
+ def setup(app):
88
+ if app.verbosity >= 1:
89
+ logger.setLevel(std_logging.INFO)
90
+ else:
91
+ logger.setLevel(std_logging.WARNING)
92
+
93
+ logger.info("[fastgpx-doc] Setting up overloaded docstring formatting")
94
+ app.connect("autodoc-process-docstring", format_overloaded_docstring)
@@ -4,7 +4,7 @@ build-backend = "scikit_build_core.build"
4
4
 
5
5
  [project]
6
6
  name = "fastgpx"
7
- version = "0.2.2"
7
+ version = "0.4.0"
8
8
  requires-python = ">=3.11"
9
9
 
10
10
  description = "An experimental Python library for parsing GPX files fast."
@@ -43,10 +43,14 @@ std::chrono::system_clock::time_point TimePoint::value() const
43
43
 
44
44
  bool TimeBounds::IsEmpty() const
45
45
  {
46
- assert(start_time.has_value() == end_time.has_value());
47
46
  return !start_time.has_value() && !end_time.has_value();
48
47
  }
49
48
 
49
+ bool TimeBounds::IsRange() const
50
+ {
51
+ return start_time.has_value() && end_time.has_value();
52
+ }
53
+
50
54
  void TimeBounds::Add(const std::chrono::system_clock::time_point time_point)
51
55
  {
52
56
  if (start_time.has_value())
@@ -380,6 +384,16 @@ Gpx ParseGpx(const std::filesystem::path& path)
380
384
  pugi::xml_node root = doc.child("gpx");
381
385
  Gpx gpx;
382
386
 
387
+ const auto metadata = root.child("metadata");
388
+ if (metadata)
389
+ {
390
+ const auto name = metadata.child("name");
391
+ if (name)
392
+ {
393
+ gpx.name.emplace(name.text().as_string());
394
+ }
395
+ }
396
+
383
397
  // Iterate over each <trk> element
384
398
  for (pugi::xml_node track = root.child("trk"); track; track = track.next_sibling("trk"))
385
399
  {
@@ -28,6 +28,7 @@ struct TimeBounds
28
28
  std::optional<std::chrono::system_clock::time_point> end_time = std::nullopt;
29
29
 
30
30
  bool IsEmpty() const;
31
+ bool IsRange() const;
31
32
 
32
33
  void Add(std::chrono::system_clock::time_point time_point);
33
34
  void Add(const TimeBounds& time_bounds);
@@ -111,6 +112,8 @@ private:
111
112
  struct Gpx
112
113
  {
113
114
  // <metadata>
115
+ std::optional<std::string> name; // <name>
116
+
114
117
  // <wpt>
115
118
  // <tre>
116
119
  std::vector<Track> tracks; // <trk>
@@ -33,6 +33,8 @@ TEST_CASE("Parse two-point single segment track", "[parse][simple]")
33
33
  REQUIRE(gpx.tracks.size() == 1);
34
34
  REQUIRE(gpx.tracks[0].segments.size() == 1);
35
35
 
36
+ CHECK(!gpx.name.has_value());
37
+
36
38
  CHECK_THAT(gpx.tracks[0].segments[0].GetLength2D(), WithinAbs(1.3839, kMETERS_TOL));
37
39
  CHECK_THAT(gpx.tracks[0].GetLength2D(), WithinAbs(1.3839, kMETERS_TOL));
38
40
  CHECK_THAT(gpx.GetLength2D(), WithinAbs(1.3839, kMETERS_TOL));
@@ -74,6 +76,8 @@ TEST_CASE("Parse time bounds of real world GPX file", "[parse][simple]")
74
76
  "gpx/2024 TopCamp/Connected_20240529_091916_Harald_Bothners_Veg_36_7052_Trondheim.gpx";
75
77
  const auto gpx = fastgpx::ParseGpx(path);
76
78
 
79
+ CHECK(gpx.name.value() == "Harald Bothners Veg 36, 7052 Trondheim");
80
+
77
81
  REQUIRE(gpx.tracks.size() == 1);
78
82
 
79
83
  // TimeBounds
@@ -97,6 +101,8 @@ TEST_CASE("Parse bounds of real world GPX file", "[parse][simple]")
97
101
  "gpx/2024 TopCamp/Connected_20240529_091916_Harald_Bothners_Veg_36_7052_Trondheim.gpx";
98
102
  const auto gpx = fastgpx::ParseGpx(path);
99
103
 
104
+ CHECK(gpx.name.value() == "Harald Bothners Veg 36, 7052 Trondheim");
105
+
100
106
  REQUIRE(gpx.tracks.size() == 1);
101
107
 
102
108
  const fastgpx::LatLong expected_min(61.841507, 9.090457);
@@ -53,7 +53,13 @@ PYBIND11_MODULE(fastgpx, m)
53
53
  py::arg("end_time"))
54
54
  .def_readwrite("start_time", &fastgpx::TimeBounds::start_time)
55
55
  .def_readwrite("end_time", &fastgpx::TimeBounds::end_time)
56
- .def("is_empty", &fastgpx::TimeBounds::IsEmpty);
56
+ .def("is_empty", &fastgpx::TimeBounds::IsEmpty)
57
+ .def("is_range", &fastgpx::TimeBounds::IsRange)
58
+ .def("add",
59
+ py::overload_cast<std::chrono::system_clock::time_point>(&fastgpx::TimeBounds::Add),
60
+ py::arg("datetime"))
61
+ .def("add", py::overload_cast<const fastgpx::TimeBounds&>(&fastgpx::TimeBounds::Add),
62
+ py::arg("timebounds"));
57
63
 
58
64
  py::class_<fastgpx::LatLong>(m, "LatLong")
59
65
  .def(py::init<>())
@@ -197,6 +203,7 @@ PYBIND11_MODULE(fastgpx, m)
197
203
  py::class_<fastgpx::Gpx>(m, "Gpx")
198
204
  .def(py::init<>()) // Default constructor
199
205
  .def_readwrite("tracks", &fastgpx::Gpx::tracks)
206
+ .def_readwrite("name", &fastgpx::Gpx::name)
200
207
  .def("bounds", &fastgpx::Gpx::GetBounds)
201
208
  .def("get_bounds", &fastgpx::Gpx::GetBounds) // gpxpy compatiblity
202
209
  .def("time_bounds", &fastgpx::Gpx::GetTimeBounds)
@@ -27,6 +27,15 @@
27
27
  namespace PYBIND11_NAMESPACE {
28
28
  namespace detail {
29
29
 
30
+ inline std::time_t to_utc_time_t(std::tm* tm)
31
+ {
32
+ #if defined(_WIN32)
33
+ return _mkgmtime(tm);
34
+ #else
35
+ return timegm(tm);
36
+ #endif
37
+ }
38
+
30
39
  template<typename type>
31
40
  class duration_caster
32
41
  {
@@ -195,7 +204,10 @@ public:
195
204
  return false;
196
205
  }
197
206
 
198
- value = time_point_cast<Duration>(system_clock::from_time_t(std::mktime(&cal)) + msecs);
207
+ // HACK: Assume datetime.datetime objects are in UTC, so we use timegm instead of mktime.
208
+ // value = time_point_cast<Duration>(system_clock::from_time_t(std::mktime(&cal)) + msecs);
209
+ std::time_t tt = to_utc_time_t(&cal);
210
+ value = time_point_cast<Duration>(system_clock::from_time_t(tt) + msecs);
199
211
  return true;
200
212
  }
201
213
 
@@ -9,4 +9,4 @@ from fastgpx.fastgpx import geo
9
9
  from fastgpx.fastgpx import parse
10
10
  from fastgpx.fastgpx import polyline
11
11
  from . import fastgpx
12
- __all__ = ['Bounds', 'Gpx', 'LatLong', 'Segment', 'TimeBounds', 'Track', 'fastgpx', 'geo', 'parse', 'polyline']
12
+ __all__: list = ['Bounds', 'Gpx', 'LatLong', 'Segment', 'TimeBounds', 'Track', 'geo', 'parse', 'polyline']
@@ -51,6 +51,7 @@ class Bounds:
51
51
  def min_longitude(self, arg1: float) -> None:
52
52
  ...
53
53
  class Gpx:
54
+ name: str | None
54
55
  tracks: list[Track]
55
56
  def __init__(self) -> None:
56
57
  ...
@@ -101,8 +102,16 @@ class TimeBounds:
101
102
  @typing.overload
102
103
  def __init__(self, start_time: datetime.datetime | None, end_time: datetime.datetime | None) -> None:
103
104
  ...
105
+ @typing.overload
106
+ def add(self, datetime: datetime.datetime) -> None:
107
+ ...
108
+ @typing.overload
109
+ def add(self, timebounds: TimeBounds) -> None:
110
+ ...
104
111
  def is_empty(self) -> bool:
105
112
  ...
113
+ def is_range(self) -> bool:
114
+ ...
106
115
  class Track:
107
116
  comment: str | None
108
117
  description: str | None
@@ -1,7 +1,6 @@
1
1
  import datetime
2
2
 
3
3
  import gpxpy
4
- import gpxpy.gpx
5
4
  import pytest
6
5
 
7
6
  import fastgpx
@@ -53,6 +52,25 @@ def test_segment_length2d(gpx_path: str):
53
52
  distance = gpx.tracks[0].segments[0].length_2d()
54
53
  assert distance == pytest.approx(17809.2701, abs=METERS_TOL)
55
54
 
55
+ # fastgpx.Gpx.name
56
+
57
+
58
+ def test_gpx_name_missing():
59
+ path = 'gpx/test/debug-segment.gpx'
60
+ gpx = fastgpx.parse(path)
61
+ assert gpx.name is None
62
+
63
+
64
+ def test_gpx_name_empty_string(gpx_path: str):
65
+ gpx = fastgpx.parse(gpx_path)
66
+ assert gpx.name == ''
67
+
68
+
69
+ def test_gpx_name():
70
+ path = 'gpx/test/two-points.gpx'
71
+ gpx = fastgpx.parse(path)
72
+ assert gpx.name == 'Two Point Segment'
73
+
56
74
  # fastgpx.Gpx.bounds
57
75
 
58
76
 
@@ -0,0 +1,73 @@
1
+ from datetime import datetime
2
+
3
+ import fastgpx
4
+
5
+
6
+ class TestTimeBounds:
7
+
8
+ def test_init_defaults(self):
9
+ bounds = fastgpx.TimeBounds()
10
+ assert bounds.is_empty()
11
+ assert not bounds.is_range()
12
+ assert bounds.start_time is None
13
+ assert bounds.end_time is None
14
+
15
+ def test_init_with_start_time(self):
16
+ start_time = datetime.fromisoformat("2025-06-20 08:07:28+00:00")
17
+ bounds = fastgpx.TimeBounds(start_time=start_time, end_time=None)
18
+ assert not bounds.is_empty()
19
+ assert not bounds.is_range()
20
+ assert bounds.start_time == start_time
21
+ assert bounds.end_time is None
22
+
23
+ def test_init_with_end_time(self):
24
+ end_time = datetime.fromisoformat("2025-06-27 13:37:00+00:00")
25
+ bounds = fastgpx.TimeBounds(start_time=None, end_time=end_time)
26
+ assert not bounds.is_empty()
27
+ assert not bounds.is_range()
28
+ assert bounds.start_time is None
29
+ assert bounds.end_time == end_time
30
+
31
+ def test_init_with_start_and_end_time(self):
32
+ start_time = datetime.fromisoformat("2025-06-20 08:07:28+00:00")
33
+ end_time = datetime.fromisoformat("2025-06-27 13:37:00+00:00")
34
+ bounds = fastgpx.TimeBounds(start_time=start_time, end_time=end_time)
35
+ assert not bounds.is_empty()
36
+ assert bounds.is_range()
37
+ assert bounds.start_time == start_time
38
+ assert bounds.end_time == end_time
39
+
40
+ def test_add_single_time_point(self):
41
+ bounds = fastgpx.TimeBounds()
42
+ time_point = datetime.fromisoformat("2025-06-20 08:07:28+00:00")
43
+ bounds.add(time_point)
44
+ assert not bounds.is_empty()
45
+ assert bounds.is_range()
46
+ assert bounds.start_time == time_point
47
+ assert bounds.end_time == time_point
48
+
49
+ def test_add_second_time_point(self):
50
+ bounds = fastgpx.TimeBounds()
51
+ time_point1 = datetime.fromisoformat("2025-06-20 08:07:28+00:00")
52
+ time_point2 = datetime.fromisoformat("2025-06-27 13:37:00+00:00")
53
+ bounds.add(time_point1)
54
+ bounds.add(time_point2)
55
+ assert not bounds.is_empty()
56
+ assert bounds.is_range()
57
+ assert bounds.start_time == time_point1
58
+ assert bounds.end_time == time_point2
59
+
60
+ def test_add_time_bounds(self):
61
+ bounds1 = fastgpx.TimeBounds(
62
+ start_time=datetime.fromisoformat("2025-06-20 08:07:28+00:00"),
63
+ end_time=datetime.fromisoformat("2025-06-27 13:37:00+00:00")
64
+ )
65
+ bounds2 = fastgpx.TimeBounds(
66
+ start_time=datetime.fromisoformat("2025-06-25 10:00:00+00:00"),
67
+ end_time=datetime.fromisoformat("2025-06-30 15:00:00+00:00")
68
+ )
69
+ bounds1.add(bounds2)
70
+ assert not bounds1.is_empty()
71
+ assert bounds1.is_range()
72
+ assert bounds1.start_time == bounds1.start_time
73
+ assert bounds1.end_time == bounds2.end_time
@@ -97,7 +97,7 @@ wheels = [
97
97
 
98
98
  [[package]]
99
99
  name = "fastgpx"
100
- version = "0.0.1.dev0"
100
+ version = "0.4.0"
101
101
  source = { editable = "." }
102
102
 
103
103
  [package.dev-dependencies]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes