Skip to content

Commit b6253a8

Browse files
committed
chg: improve memory usage and avoid repetitive work
1 parent 4bd5f98 commit b6253a8

12 files changed

Lines changed: 186 additions & 34 deletions

File tree

src/gps_logger_parser/accelerometer/__init__.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,15 @@ class AcceleratorParser(AccelerometerHarmonizationMixin, Parser):
3131
AccelerometerHarmonizedColumn.Z: "Z",
3232
}
3333

34+
@classmethod
35+
def can_parse(cls, parsable):
36+
"""Check if the file starts with the expected HEAD bytes."""
37+
try:
38+
with parsable.get_stream(binary=False) as stream:
39+
return stream_starts_with(stream, cls.HEAD)
40+
except (UnicodeDecodeError, OSError):
41+
return False
42+
3443
def __init__(self, parsable: Parsable):
3544
super().__init__(parsable)
3645

@@ -46,7 +55,7 @@ def __init__(self, parsable: Parsable):
4655

4756
stream.seek(0)
4857

49-
for row in stream.readlines():
58+
for row in stream:
5059
if [v.strip() for v in row.split(",")] == self.FIELDS:
5160
break
5261
row_count += 1

src/gps_logger_parser/gps/axytrek.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,10 @@ def __init__(self, parsable: Parsable):
8989
parse_options = pacsv.ParseOptions(
9090
delimiter=self.SEPARATOR, invalid_row_handler=skip
9191
)
92-
with self.file.get_stream(binary=True) as stream:
93-
self.data = pacsv.read_csv(stream, parse_options=parse_options).to_pandas()
92+
with self.file.get_stream(binary=True) as binary_stream:
93+
self.data = pacsv.read_csv(
94+
binary_stream, parse_options=parse_options
95+
).to_pandas()
9496

9597

9698
PARSERS = [

src/gps_logger_parser/gps/catlog.py

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import pandas as pd
55

6-
from ..helpers import stream_chunk_contains, stream_starts_with
6+
from ..helpers import stream_starts_with
77
from ..parser_base import CSVParser, Parsable
88
from .columns import GPSHarmonizedColumn
99
from .mixin import GPSHarmonizationMixin
@@ -54,6 +54,17 @@ class GPSCatTrackParser(GPSHarmonizationMixin, CSVParser):
5454
GPSHarmonizedColumn.TRIP_NR: None,
5555
}
5656

57+
@classmethod
58+
def can_parse(cls, parsable):
59+
"""Check if the file starts with the expected START_WITH prefix."""
60+
if not cls.START_WITH:
61+
return True
62+
try:
63+
with parsable.get_stream(binary=False) as stream:
64+
return stream_starts_with(stream, cls.START_WITH)
65+
except (UnicodeDecodeError, OSError):
66+
return False
67+
5768
def harmonize_data(self, data):
5869
# Combine Date and Time columns into timestamp
5970
data["timestamp"] = pd.to_datetime(
@@ -73,13 +84,13 @@ def __init__(self, parsable: Parsable):
7384
self._raise_not_supported("Stream must start with Name:CatLog")
7485

7586
if self.DIVIDER:
76-
if stream_chunk_contains(stream, 500, self.DIVIDER):
77-
_intro, data = stream.read().split(self.DIVIDER)
78-
content = io.StringIO(data)
79-
else:
87+
full_content = stream.read()
88+
if self.DIVIDER not in full_content:
8089
self._raise_not_supported(
8190
f"Stream doesn't have the divider {self.DIVIDER}"
8291
)
92+
_intro, data = full_content.split(self.DIVIDER, 1)
93+
content = io.StringIO(data)
8394
else:
8495
content = stream
8596

@@ -191,13 +202,13 @@ def __init__(self, parsable: Parsable):
191202
self._raise_not_supported("Stream must start with Name:CatLog")
192203

193204
if self.DIVIDER:
194-
if stream_chunk_contains(stream, 500, self.DIVIDER):
195-
_intro, data = stream.read().split(self.DIVIDER)
196-
content = io.StringIO(data)
197-
else:
205+
full_content = stream.read()
206+
if self.DIVIDER not in full_content:
198207
self._raise_not_supported(
199208
f"Stream doesn't have the divider {self.DIVIDER}"
200209
)
210+
_intro, data = full_content.split(self.DIVIDER, 1)
211+
content = io.StringIO(data)
201212
else:
202213
content = stream
203214

src/gps_logger_parser/gps/ecotone.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import csv
2+
13
import geoarrow.pyarrow as ga
24
import numpy as np
35
import pandas as pd
@@ -49,6 +51,21 @@ def _check_headers(self, header):
4951
f"{len(header)} != {len(self.FIELDS)}"
5052
)
5153

54+
@classmethod
55+
def can_parse(cls, parsable):
56+
"""Check if the CSV header has the expected number of columns."""
57+
try:
58+
with parsable.get_stream(binary=False) as stream:
59+
if not stream.seekable():
60+
return False
61+
reader = csv.reader(
62+
stream, delimiter=cls.SEPARATOR, skipinitialspace=True
63+
)
64+
header = next(reader)
65+
return len(header) == len(cls.FIELDS)
66+
except (StopIteration, UnicodeDecodeError):
67+
return False
68+
5269
def harmonize_data(self, data):
5370
# Call parent harmonization — applies MAPPINGS, enforces GPS schema,
5471
# creates geometry, and drops raw source columns
@@ -81,10 +98,8 @@ def harmonize_data(self, data):
8198
data["meters_north"].str[:-1].astype(float).values,
8299
crs="EPSG:32633", # UTM zone 33N
83100
)
84-
# Convert to pandas ArrowExtensionArray
85-
result["geometry"] = pd.array(
86-
points.to_pylist(), dtype=pd.ArrowDtype(points.type)
87-
)
101+
# Convert to pandas ArrowExtensionArray directly from Arrow array
102+
result["geometry"] = pd.array(points, dtype=pd.ArrowDtype(points.type))
88103
else:
89104
# Create empty geometry column if lat/lon don't exist or are all null
90105
empty_points = ga.make_point(
@@ -93,7 +108,7 @@ def harmonize_data(self, data):
93108
crs="EPSG:32633", # UTM zone 33N
94109
)
95110
result["geometry"] = pd.array(
96-
empty_points.to_pylist(), dtype=pd.ArrowDtype(empty_points.type)
111+
empty_points, dtype=pd.ArrowDtype(empty_points.type)
97112
)
98113

99114
return result

src/gps_logger_parser/gps/gpx.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,17 @@ class GPXParser(GPSHarmonizationMixin, Parser):
4242
GPSHarmonizedColumn.TRIP_NR: None,
4343
}
4444

45+
@classmethod
46+
def can_parse(cls, parsable):
47+
"""Check if the file starts with <?xml within the first 30 bytes."""
48+
try:
49+
with parsable.get_stream(binary=False) as stream:
50+
if not stream.seekable():
51+
return False
52+
return stream_chunk_contains(stream, 30, "<?xml")
53+
except (UnicodeDecodeError, OSError):
54+
return False
55+
4556
def harmonize_data(self, data):
4657
data["time"] = pd.to_datetime(data["time"], utc=True)
4758
return super().harmonize_data(data)
@@ -61,6 +72,6 @@ def __init__(self, stream):
6172
for track in gpx.tracks:
6273
for segment in track.segments:
6374
for point in segment.points:
64-
points.append(getattr(point, f) for f in self.FIELDS)
75+
points.append(tuple(getattr(point, f) for f in self.FIELDS))
6576

6677
self.data = pd.DataFrame(points, columns=self.FIELDS)

src/gps_logger_parser/gps/jm.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,15 @@ class GPS2JMParser7_5(GPSHarmonizationMixin, Parser):
6868
GPSHarmonizedColumn.TRIP_NR: None,
6969
}
7070

71+
@classmethod
72+
def can_parse(cls, parsable):
73+
"""Check if the file contains the 2JmGPS-LOG marker."""
74+
try:
75+
with parsable.get_stream(binary=False, errors="backslashreplace") as stream:
76+
return stream_chunk_contains(stream, 30, "2JmGPS-LOG")
77+
except (UnicodeDecodeError, OSError):
78+
return False
79+
7180
def harmonize_data(self, data):
7281
# Call parent harmonization — applies MAPPINGS, enforces GPS schema,
7382
# creates geometry, and drops raw source columns
@@ -276,8 +285,18 @@ class GPS2JMParser8Alternative(GPSHarmonizationMixin, Parser):
276285
GPSHarmonizedColumn.TRIP_NR: None,
277286
}
278287

279-
def harmonize_data(self, data):
288+
@classmethod
289+
def can_parse(cls, parsable):
290+
"""Check if the file contains the GPS DATA marker."""
291+
try:
292+
with parsable.get_stream(binary=False, errors="backslashreplace") as stream:
293+
return stream_chunk_contains(
294+
stream, 50, "************* GPS DATA *************"
295+
)
296+
except (UnicodeDecodeError, OSError):
297+
return False
280298

299+
def harmonize_data(self, data):
281300
# Call parent harmonization — applies MAPPINGS, enforces GPS schema,
282301
# creates geometry, and drops raw source columns
283302
result = super().harmonize_data(data)

src/gps_logger_parser/gps/mixin.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,8 @@ def _create_geometry_column(self, data):
6969
data["latitude"].values,
7070
crs="EPSG:4326",
7171
)
72-
# Convert to pandas ArrowExtensionArray
73-
data["geometry"] = pd.array(
74-
points.to_pylist(), dtype=pd.ArrowDtype(points.type)
75-
)
72+
# Convert to pandas ArrowExtensionArray directly from the Arrow array
73+
data["geometry"] = pd.array(points, dtype=pd.ArrowDtype(points.type))
7674
else:
7775
# Create empty geometry column if lat/lon don't exist or are all null
7876
empty_points = ga.make_point(
@@ -81,7 +79,7 @@ def _create_geometry_column(self, data):
8179
crs="EPSG:4326",
8280
)
8381
data["geometry"] = pd.array(
84-
empty_points.to_pylist(), dtype=pd.ArrowDtype(empty_points.type)
82+
empty_points, dtype=pd.ArrowDtype(empty_points.type)
8583
)
8684

8785
return data

src/gps_logger_parser/gps/pathtrack.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,17 @@ class PathtrackParser(GPSHarmonizationMixin, Parser):
5353
GPSHarmonizedColumn.TRIP_NR: None,
5454
}
5555

56+
@classmethod
57+
def can_parse(cls, parsable):
58+
"""Check if the file starts with the expected HEAD bytes."""
59+
try:
60+
with parsable.get_stream(binary=False) as stream:
61+
if not stream.seekable():
62+
return False
63+
return stream_starts_with(stream, cls.HEAD)
64+
except (UnicodeDecodeError, OSError):
65+
return False
66+
5667
def harmonize_data(self, data):
5768
# Combine date and time fields into timestamp
5869
data["timestamp"] = pd.to_datetime(

src/gps_logger_parser/gps/unknown.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,22 @@ class GPSUnknownFormatParserWithEmptyColumns(GPSHarmonizationMixin, Parser):
7878

7979
MAPPINGS = MAPPINGS
8080

81+
@classmethod
82+
def can_parse(cls, parsable):
83+
"""Check if the header (with empty columns filtered) matches FIELDS."""
84+
try:
85+
with parsable.get_stream(binary=False) as stream:
86+
if not stream.seekable():
87+
return False
88+
reader = csv.reader(
89+
stream, delimiter=cls.SEPARATOR, skipinitialspace=True
90+
)
91+
header = next(reader)
92+
header = [c for c in header if c != ""]
93+
return header == cls.FIELDS
94+
except (StopIteration, UnicodeDecodeError):
95+
return False
96+
8197
def harmonize_data(self, data):
8298
# Combine Date and Time columns into timestamp
8399
data["timestamp"] = pd.to_datetime(

src/gps_logger_parser/parser.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ def detect_file(path: UPath, *args, logger=logger, **kwargs):
2121

2222
for parser in available_parsers:
2323
try:
24+
if not parser.can_parse(parsable):
25+
logger.debug(f"Skipped {parser.__name__}: can_parse returned False")
26+
continue
2427
result = parser(parsable)
2528
logger.info(f"Parsed with {parser}")
2629
return result

0 commit comments

Comments
 (0)