Skip to content

Commit 2444da3

Browse files
committed
MOD: Improve path validation for timeseries
1 parent e2a8184 commit 2444da3

File tree

5 files changed

+88
-4
lines changed

5 files changed

+88
-4
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
#### Bug Fixes
66
- Fixed and issue where the `end` parameter in `timeseries.get_range_async` did not support a value of `None`
7+
- Fixed an issue where `timeseries.get_range` requests would begin with an invalid `path` parameter
78

89
## 0.15.1 - 2023-07-06
910

databento/common/dbnstore.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
from databento.common.data import STRUCT_MAP
3838
from databento.common.error import BentoError
3939
from databento.common.symbology import InstrumentIdMappingInterval
40+
from databento.common.validation import validate_file_write_path
4041
from databento.common.validation import validate_maybe_enum
4142
from databento.live import DBNRecord
4243

@@ -978,10 +979,20 @@ def to_file(self, path: Path | str) -> None:
978979
path : str
979980
The file path to write to.
980981
982+
Raises
983+
------
984+
IsADirectoryError
985+
If path is a directory.
986+
FileExistsError
987+
If path exists.
988+
PermissionError
989+
If path is not writable.
990+
981991
"""
982-
with open(path, mode="xb") as f:
992+
file_path = validate_file_write_path(path, "path")
993+
with open(file_path, mode="xb") as f:
983994
f.write(self._data_source.reader.read())
984-
self._data_source = FileDataSource(path)
995+
self._data_source = FileDataSource(file_path)
985996

986997
def to_json(
987998
self,

databento/common/validation.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import os
34
from enum import Enum
45
from os import PathLike
56
from pathlib import Path
@@ -27,6 +28,11 @@ def validate_path(value: PathLike[str] | str, param: str) -> Path:
2728
Path
2829
A valid path.
2930
31+
Raises
32+
------
33+
TypeError
34+
If value is not a valid path.
35+
3036
"""
3137
try:
3238
return Path(value)
@@ -37,6 +43,42 @@ def validate_path(value: PathLike[str] | str, param: str) -> Path:
3743
) from None
3844

3945

46+
def validate_file_write_path(value: PathLike[str] | str, param: str) -> Path:
47+
"""
48+
Validate whether the given value is a valid path to a writable file.
49+
50+
Parameters
51+
----------
52+
value: PathLike or str
53+
The value to validate.
54+
param : str
55+
The name of the parameter being validated (for any error message).
56+
57+
Returns
58+
-------
59+
Path
60+
A valid path to a writable file.
61+
62+
Raises
63+
------
64+
IsADirectoryError
65+
If path is a directory.
66+
FileExistsError
67+
If path exists.
68+
PermissionError
69+
If path is not writable.
70+
71+
"""
72+
path_valid = validate_path(value, param)
73+
if not os.access(path_valid.parent, os.W_OK):
74+
raise PermissionError(f"The file `{value}` is not writable.")
75+
if path_valid.is_dir():
76+
raise IsADirectoryError(f"The `{param}` was not a path to a file.")
77+
if path_valid.is_file():
78+
raise FileExistsError(f"The file `{value}` already exists.")
79+
return path_valid
80+
81+
4082
def validate_enum(
4183
value: object,
4284
enum: type[E],

databento/historical/api/timeseries.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from databento.common.parsing import optional_datetime_to_string
1616
from databento.common.parsing import optional_symbols_list_to_list
1717
from databento.common.validation import validate_enum
18+
from databento.common.validation import validate_file_write_path
1819
from databento.common.validation import validate_semantic_string
1920
from databento.historical.api import API_VERSION
2021
from databento.historical.http import BentoHttpAPI
@@ -79,7 +80,7 @@ def get_range(
7980
limit : int, optional
8081
The maximum number of records to return. If `None` then no limit.
8182
path : PathLike or str, optional
82-
The path to stream the data to on disk (will then return a `DBNStore`).
83+
The file path to stream the data to on disk (will then return a `DBNStore`).
8384
8485
Returns
8586
-------
@@ -115,6 +116,8 @@ def get_range(
115116
data["limit"] = str(limit)
116117
if end is not None:
117118
data["end"] = end_valid
119+
if path is not None:
120+
path = validate_file_write_path(path, "path")
118121

119122
return self._stream(
120123
url=self._base_url + ".get_range",
@@ -174,7 +177,7 @@ async def get_range_async(
174177
limit : int, optional
175178
The maximum number of records to return. If `None` then no limit.
176179
path : PathLike or str, optional
177-
The path to stream the data to on disk (will then return a `DBNStore`).
180+
The file path to stream the data to on disk (will then return a `DBNStore`).
178181
179182
Returns
180183
-------
@@ -210,6 +213,8 @@ async def get_range_async(
210213
data["limit"] = str(limit)
211214
if end is not None:
212215
data["end"] = end_valid
216+
if path is not None:
217+
path = validate_file_write_path(path, "path")
213218

214219
return await self._stream_async(
215220
url=self._base_url + ".get_range",

tests/test_common_validation.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
from __future__ import annotations
22

33
from enum import Enum
4+
from pathlib import Path
45
from typing import Any
56

67
import pytest
78
from databento.common.validation import validate_enum
9+
from databento.common.validation import validate_file_write_path
810
from databento.common.validation import validate_gateway
911
from databento.common.validation import validate_maybe_enum
1012
from databento.common.validation import validate_path
@@ -26,6 +28,29 @@ def test_validate_path_given_wrong_types_raises_type_error(
2628
with pytest.raises(TypeError):
2729
validate_path(value, "param")
2830

31+
def test_validate_file_write_path(
32+
tmp_path: Path,
33+
) -> None:
34+
# Arrange, Act, Assert
35+
test_file = tmp_path / "test.file"
36+
validate_file_write_path(test_file, "param")
37+
38+
def test_validate_file_write_path_is_dir(
39+
tmp_path: Path,
40+
) -> None:
41+
# Arrange, Act, Assert
42+
with pytest.raises(IsADirectoryError):
43+
validate_file_write_path(tmp_path, "param")
44+
45+
def test_validate_file_write_path_exists(
46+
tmp_path: Path,
47+
) -> None:
48+
# Arrange, Act, Assert
49+
test_file = tmp_path / "test.file"
50+
test_file.touch()
51+
with pytest.raises(FileExistsError):
52+
validate_file_write_path(test_file, "param")
53+
2954
@pytest.mark.parametrize(
3055
"value, enum",
3156
[

0 commit comments

Comments
 (0)