Skip to content

Commit acb428a

Browse files
authored
Merge branch 'main' into task/8376-write-test-to-filter-displayed-trackss
2 parents 69f3263 + b12710e commit acb428a

191 files changed

Lines changed: 1711 additions & 1342 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/benchmark.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ jobs:
4040
- name: Install the project
4141
shell: bash
4242
run: uv sync --locked --dev
43-
- name: Run regression tests
44-
run: uv run pytest ./tests/benchmark_otanalytics.py --benchmark-json output.json
43+
- name: Run benchmark tests
44+
run: uv run pytest ./tests/benchmark --benchmark-json output.json
4545
- name: Store benchmark result
4646
uses: benchmark-action/github-action-benchmark@v1
4747
with:

.github/workflows/regression-test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,4 @@ jobs:
3939
shell: bash
4040
run: uv sync --locked --dev
4141
- name: Run regression tests
42-
run: uv run pytest ./tests/regression_otanalytics.py
42+
run: uv run pytest ./tests/regression

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,4 @@ jobs:
3131
with:
3232
py-version: ${{ matrix.py }}
3333
package-path: OTAnalytics
34-
test-path: tests/OTAnalytics
34+
test-path: tests/unit

.pre-commit-config.yaml

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,10 @@ repos:
1414
- id: requirements-txt-fixer
1515
- id: check-executables-have-shebangs
1616
- id: detect-private-key
17-
- repo: local
17+
- repo: https://github.com/OpenTrafficCam/dependency-update-pre-commit
18+
rev: v0.2.0
1819
hooks:
19-
- id: update-type-stubs
20-
name: Check for Type Stubs and Update Config
21-
entry: ./update_precommit.py
22-
language: system
23-
files: ^requirements.*\.txt$
24-
stages:
25-
- pre-commit
20+
- id: update-mypy-dependencies
2621
- repo: https://github.com/PyCQA/flake8
2722
rev: 7.3.0
2823
hooks:
@@ -45,36 +40,38 @@ repos:
4540
entry: mypy OTAnalytics tests --config-file=pyproject.toml
4641
additional_dependencies:
4742
- av==13.0.0
48-
- black==24.8.0
43+
- black==25.1.0
4944
- customtkinter==5.2.2
50-
- fastapi==0.116.2
51-
- hatch-requirements-txt==0.4.1
45+
- fastapi==0.128.0
5246
- ijson==3.3.0
5347
- interrogate==1.7.0
54-
- isort==5.13.2
48+
- isort==6.0.1
5549
- matplotlib==3.10.0
5650
- more-itertools==10.7.0
5751
- mypy==1.16.1
5852
- nicegui==2.24.1
5953
- numpy==2.1.1
6054
- opencv-python-headless==4.10.0.84
61-
- pandas-stubs==2.2.3.241126
55+
- pandas-stubs==2.3.2.250926
6256
- pandas==2.3.3
57+
- playwright==1.55.0
6358
- polars==1.32.3
6459
- pre-commit==4.1.0
6560
- pyarrow==21.0.0
6661
- pytest-asyncio==0.24.0
6762
- pytest-benchmark==5.1.0
68-
- pytest-cov==6.0.0
69-
- pytest-lazy-fixtures==1.1.2
63+
- pytest-cov==7.0.0
64+
- pytest-lazy-fixtures==1.4.0
65+
- pytest-playwright==0.7.1
66+
- pytest-timeout==2.4.0
7067
- pytest==8.3.4
71-
- starlette==0.48.0
72-
- twine==6.1.0
68+
- starlette==0.49.1
69+
- twine==6.2.0
7370
- types-PyYAML
7471
- types-flake8
7572
- types-openpyxl
7673
- types-pillow
77-
- types-pywin32
74+
- types-pywin32; sys_platform == "win32"
7875
- types-requests
7976
- types-seaborn
8077
- types-selenium
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<component name="ProjectRunConfigurationManager">
2+
<configuration default="false" name="pytest-acceptance-tests" type="tests" factoryName="py.test">
3+
<module name="OTAnalytics"/>
4+
<option name="INTERPRETER_OPTIONS" value=""/>
5+
<option name="PARENT_ENVS" value="true"/>
6+
<option name="SDK_HOME" value=""/>
7+
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$"/>
8+
<option name="IS_MODULE_SDK" value="true"/>
9+
<option name="ADD_CONTENT_ROOTS" value="true"/>
10+
<option name="ADD_SOURCE_ROOTS" value="true"/>
11+
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py"/>
12+
<option name="_new_keywords" value="&quot;&quot;"/>
13+
<option name="_new_parameters" value="&quot;&quot;"/>
14+
<option name="_new_additionalArguments" value="&quot;&quot;"/>
15+
<option name="_new_target" value="&quot;$PROJECT_DIR$/tests/acceptance&quot;"/>
16+
<option name="_new_targetType" value="&quot;PATH&quot;"/>
17+
<method v="2"/>
18+
</configuration>
19+
</component>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<component name="ProjectRunConfigurationManager">
2+
<configuration default="false" name="pytest-benchmark-tests" type="tests" factoryName="py.test">
3+
<module name="OTAnalytics"/>
4+
<option name="INTERPRETER_OPTIONS" value=""/>
5+
<option name="PARENT_ENVS" value="true"/>
6+
<option name="SDK_HOME" value=""/>
7+
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$"/>
8+
<option name="IS_MODULE_SDK" value="true"/>
9+
<option name="ADD_CONTENT_ROOTS" value="true"/>
10+
<option name="ADD_SOURCE_ROOTS" value="true"/>
11+
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py"/>
12+
<option name="_new_keywords" value="&quot;&quot;"/>
13+
<option name="_new_parameters" value="&quot;&quot;"/>
14+
<option name="_new_additionalArguments" value="&quot;&quot;"/>
15+
<option name="_new_target" value="&quot;$PROJECT_DIR$/tests/benchmark&quot;"/>
16+
<option name="_new_targetType" value="&quot;PATH&quot;"/>
17+
<method v="2"/>
18+
</configuration>
19+
</component>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<component name="ProjectRunConfigurationManager">
2+
<configuration default="false" name="pytest-regression-tests" type="tests" factoryName="py.test">
3+
<module name="OTAnalytics"/>
4+
<option name="INTERPRETER_OPTIONS" value=""/>
5+
<option name="PARENT_ENVS" value="true"/>
6+
<option name="SDK_HOME" value=""/>
7+
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$"/>
8+
<option name="IS_MODULE_SDK" value="true"/>
9+
<option name="ADD_CONTENT_ROOTS" value="true"/>
10+
<option name="ADD_SOURCE_ROOTS" value="true"/>
11+
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py"/>
12+
<option name="_new_keywords" value="&quot;&quot;"/>
13+
<option name="_new_parameters" value="&quot;&quot;"/>
14+
<option name="_new_additionalArguments" value="&quot;&quot;"/>
15+
<option name="_new_target" value="&quot;$PROJECT_DIR$/tests/regression&quot;"/>
16+
<option name="_new_targetType" value="&quot;PATH&quot;"/>
17+
<method v="2"/>
18+
</configuration>
19+
</component>

.run/pytest-unit-tests.run.xml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<component name="ProjectRunConfigurationManager">
2+
<configuration default="false" name="pytest-unit-tests" type="tests" factoryName="py.test">
3+
<module name="OTAnalytics"/>
4+
<option name="INTERPRETER_OPTIONS" value=""/>
5+
<option name="PARENT_ENVS" value="true"/>
6+
<option name="SDK_HOME" value=""/>
7+
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$"/>
8+
<option name="IS_MODULE_SDK" value="true"/>
9+
<option name="ADD_CONTENT_ROOTS" value="true"/>
10+
<option name="ADD_SOURCE_ROOTS" value="true"/>
11+
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py"/>
12+
<option name="_new_keywords" value="&quot;&quot;"/>
13+
<option name="_new_parameters" value="&quot;&quot;"/>
14+
<option name="_new_additionalArguments" value="&quot;&quot;"/>
15+
<option name="_new_target" value="&quot;$PROJECT_DIR$/tests/unit&quot;"/>
16+
<option name="_new_targetType" value="&quot;PATH&quot;"/>
17+
<method v="2"/>
18+
</configuration>
19+
</component>

OTAnalytics/adapter_ui/dummy_viewmodel.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
)
6969
from OTAnalytics.application.export_formats.export_mode import OVERWRITE
7070
from OTAnalytics.application.logger import logger
71+
from OTAnalytics.application.parser.config_parser import StartDateMissing
7172
from OTAnalytics.application.parser.flow_parser import FlowParser
7273
from OTAnalytics.application.playback import SkipTime
7374
from OTAnalytics.application.project import (
@@ -87,7 +88,7 @@
8788
SvzMetadata,
8889
WeatherType,
8990
)
90-
from OTAnalytics.application.use_cases.config import MissingDate
91+
from OTAnalytics.application.use_cases.config import ConfigValidationError
9192
from OTAnalytics.application.use_cases.config_has_changed import NoExistingConfigFound
9293
from OTAnalytics.application.use_cases.cut_tracks_with_sections import CutTracksDto
9394
from OTAnalytics.application.use_cases.editor.section_editor import (
@@ -647,13 +648,27 @@ def _save_otconfig(self, otconfig_file: Path) -> None:
647648
)
648649
self.__show_error(message)
649650
return
650-
except MissingDate:
651+
except ConfigValidationError as e:
651652
message = (
652653
f"{MESSAGE_CONFIGURATION_NOT_SAVED}"
653-
f"Start date is missing or invalid. Please add a valid start date."
654+
f"Please fix the following issues:\n"
655+
+ "\n".join(f"• {err}" for err in e.errors)
654656
)
655657
self.__show_error(message)
656658
return
659+
except StartDateMissing:
660+
# This comes from parser during serialization
661+
message = (
662+
f"{MESSAGE_CONFIGURATION_NOT_SAVED}"
663+
f"Start date and time are required for saving configuration."
664+
)
665+
self.__show_error(message)
666+
return
667+
except Exception as e:
668+
message = f"{MESSAGE_CONFIGURATION_NOT_SAVED}An unexpected error occurred."
669+
logger().exception("Failed to save configuration file", exc_info=e)
670+
self.__show_error(message)
671+
return
657672

658673
def _get_window_position(self) -> tuple[int, int]:
659674
return self.get_position()
@@ -908,6 +923,15 @@ def _save_otflow(self, otflow_file: Path) -> None:
908923
initial_position=position,
909924
)
910925
return
926+
except Exception as e:
927+
position = self.treeview_sections.get_position()
928+
message = "Failed to save sections file.\nAn unexpected error occurred."
929+
logger().exception("Failed to save sections file", exc_info=e)
930+
self._ui_factory.info_box(
931+
message=message,
932+
initial_position=position,
933+
)
934+
return
911935

912936
def cancel_action(self) -> None:
913937
self._finish_action()

OTAnalytics/application/analysis/traffic_counting.py

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
RoadUserAssignments,
1717
)
1818
from OTAnalytics.application.analysis.traffic_counting_specification import (
19+
CountingEvent,
1920
CountingSpecificationDto,
2021
ExportCounts,
2122
ExportFormat,
@@ -488,12 +489,17 @@ def create_tag(self, assignment: RoadUserAssignment) -> Tag:
488489

489490

490491
class TimeslotTagger(Tagger):
491-
def __init__(self, interval: timedelta) -> None:
492+
def __init__(
493+
self,
494+
interval: timedelta,
495+
counting_event: CountingEvent = CountingEvent.START,
496+
) -> None:
492497
self._interval = interval
498+
self._counting_event = counting_event
493499

494500
def create_tag(self, assignment: RoadUserAssignment) -> Tag:
495501
return create_timeslot_tag(
496-
assignment.events.start.interpolated_occurrence, self._interval
502+
aggregation_time(assignment, self._counting_event), self._interval
497503
)
498504

499505

@@ -881,7 +887,8 @@ def create_tagger(self, specification: CountingSpecificationDto) -> Tagger:
881887
"""
882888
mode_tagger = ModeTagger()
883889
time_tagger = TimeslotTagger(
884-
timedelta(minutes=specification.interval_in_minutes)
890+
timedelta(minutes=specification.interval_in_minutes),
891+
counting_event=specification.counting_event,
885892
)
886893
return CombinedTagger(mode_tagger, time_tagger)
887894

@@ -969,6 +976,14 @@ def end_of(rua: RoadUserAssignment) -> datetime:
969976
return rua.events.end.interpolated_occurrence
970977

971978

979+
def aggregation_time(
980+
rua: RoadUserAssignment, counting_event: CountingEvent
981+
) -> datetime:
982+
if counting_event == CountingEvent.END:
983+
return end_of(rua)
984+
return start_of(rua)
985+
986+
972987
class TrafficCounting:
973988
"""
974989
Use case to produce traffic counts.
@@ -1031,19 +1046,22 @@ def __assignment_filter_for(
10311046
specification: CountingSpecificationDto,
10321047
) -> Callable[[RoadUserAssignment], bool]:
10331048
"""Create a filter interval using the given specifications start and end time
1034-
as lower and upper bounds respectively.
1049+
as lower and upper bounds respectively. The interval is always closed at the
1050+
start time and open at the end time: [specification-start, specification-end).
1051+
The start time is always inside the interval while the end time is always
1052+
outside the interval.
10351053
10361054
The resulting filter determines whether RoadUserAssignments fall into the
10371055
desired observation interval and should be counted.
10381056
10391057
The is_upper/lower_strict parameters of this TrafficCounting object
10401058
can be used to configure the filter.
10411059
A strict bound does not allow the assignment to overlap the respective bound.
1042-
The four combinations are as follows (open = '('; closed = '['):
1043-
- [lower, upper]: both start and end of the rua must be inside the interval
1044-
- [lower, upper): the start of the rua must be inside the interval
1045-
- (lower, upper]: the end of the rua must be inside the interval
1046-
- (lower, upper): either start or end of the rua must be in the interval,
1060+
The four combinations are as follows:
1061+
- (True, True): both start and end of the rua must be inside the interval
1062+
- (True, False): the start of the rua must be inside the interval
1063+
- (False, True): the end of the rua must be inside the interval
1064+
- (False, False): either start or end of the rua must be in the interval,
10471065
or [start, end] must enclose the interval
10481066
10491067
Args:
@@ -1061,18 +1079,18 @@ def __assignment_filter_for(
10611079
is_upper_strict = self._filter_upper_bound_strict
10621080

10631081
if is_lower_strict and is_upper_strict:
1064-
return lambda rua: lower <= start_of(rua) and end_of(rua) <= upper
1082+
return lambda rua: lower <= start_of(rua) and end_of(rua) < upper
10651083

10661084
elif is_lower_strict and not is_upper_strict:
1067-
return lambda rua: lower <= start_of(rua) <= upper
1085+
return lambda rua: lower <= start_of(rua) < upper
10681086

10691087
elif not is_lower_strict and is_upper_strict:
1070-
return lambda rua: lower <= end_of(rua) <= upper
1088+
return lambda rua: lower <= end_of(rua) < upper
10711089

10721090
else:
10731091
return (
1074-
lambda rua: lower <= start_of(rua) <= upper
1075-
or lower <= end_of(rua) <= upper
1092+
lambda rua: lower <= start_of(rua) < upper
1093+
or lower <= end_of(rua) < upper
10761094
or (start_of(rua) <= lower and upper <= end_of(rua))
10771095
)
10781096

0 commit comments

Comments
 (0)