yjdata 0.1.0__tar.gz → 0.1.2__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.
- {yjdata-0.1.0 → yjdata-0.1.2}/PKG-INFO +1 -1
- {yjdata-0.1.0 → yjdata-0.1.2}/pyproject.toml +1 -1
- {yjdata-0.1.0 → yjdata-0.1.2}/src/yjdata/dataset/common.py +2 -2
- {yjdata-0.1.0 → yjdata-0.1.2}/src/yjdata/db/adapter.py +67 -12
- {yjdata-0.1.0 → yjdata-0.1.2}/src/yjdata/db/mysql.py +6 -0
- {yjdata-0.1.0 → yjdata-0.1.2}/src/yjdata/db/sql.py +35 -3
- {yjdata-0.1.0 → yjdata-0.1.2}/src/yjdata/web/calendar.py +44 -1
- {yjdata-0.1.0 → yjdata-0.1.2}/src/yjdata/web/client.py +11 -6
- {yjdata-0.1.0 → yjdata-0.1.2}/src/yjdata.egg-info/PKG-INFO +1 -1
- {yjdata-0.1.0 → yjdata-0.1.2}/src/yjdata.egg-info/SOURCES.txt +2 -1
- yjdata-0.1.2/test/test.py +4 -0
- {yjdata-0.1.0 → yjdata-0.1.2}/README.md +0 -0
- {yjdata-0.1.0 → yjdata-0.1.2}/setup.cfg +0 -0
- {yjdata-0.1.0 → yjdata-0.1.2}/src/yjdata/__init__.py +0 -0
- {yjdata-0.1.0 → yjdata-0.1.2}/src/yjdata/_optional.py +0 -0
- {yjdata-0.1.0 → yjdata-0.1.2}/src/yjdata/dataset/__init__.py +0 -0
- {yjdata-0.1.0 → yjdata-0.1.2}/src/yjdata/dataset/quote.py +0 -0
- {yjdata-0.1.0 → yjdata-0.1.2}/src/yjdata/dataset/snapshot.py +0 -0
- {yjdata-0.1.0 → yjdata-0.1.2}/src/yjdata/dataset/trade.py +0 -0
- {yjdata-0.1.0 → yjdata-0.1.2}/src/yjdata/db/__init__.py +0 -0
- {yjdata-0.1.0 → yjdata-0.1.2}/src/yjdata/db/expression.py +0 -0
- {yjdata-0.1.0 → yjdata-0.1.2}/src/yjdata/db/postgres.py +0 -0
- {yjdata-0.1.0 → yjdata-0.1.2}/src/yjdata/dtype.py +0 -0
- {yjdata-0.1.0 → yjdata-0.1.2}/src/yjdata/web/__init__.py +0 -0
- {yjdata-0.1.0 → yjdata-0.1.2}/src/yjdata/web/_context.py +0 -0
- {yjdata-0.1.0 → yjdata-0.1.2}/src/yjdata/web/_input.py +0 -0
- {yjdata-0.1.0 → yjdata-0.1.2}/src/yjdata/web/time_util.py +0 -0
- {yjdata-0.1.0 → yjdata-0.1.2}/src/yjdata.egg-info/dependency_links.txt +0 -0
- {yjdata-0.1.0 → yjdata-0.1.2}/src/yjdata.egg-info/requires.txt +0 -0
- {yjdata-0.1.0 → yjdata-0.1.2}/src/yjdata.egg-info/top_level.txt +0 -0
|
@@ -212,11 +212,11 @@ def write_chunked_parquet(
|
|
|
212
212
|
"""
|
|
213
213
|
groups = list(df.group_by(chunked_by))
|
|
214
214
|
if sort_chunks:
|
|
215
|
-
groups.sort(key=lambda g: g[0])
|
|
215
|
+
groups.sort(key=lambda g: g[0][0])
|
|
216
216
|
|
|
217
217
|
base_table = _normalize_string_type(df.head(0).to_arrow())
|
|
218
218
|
metadata = dict(base_table.schema.metadata or {})
|
|
219
|
-
metadata[metadata_key] = ','.join(str(key) for key, _ in groups).encode('utf-8')
|
|
219
|
+
metadata[metadata_key] = ','.join(str(key[0]) for key, _ in groups).encode('utf-8')
|
|
220
220
|
|
|
221
221
|
with pq.ParquetWriter(path, base_table.schema.with_metadata(metadata)) as writer:
|
|
222
222
|
for _, group in groups:
|
|
@@ -25,19 +25,43 @@ def validate_delete_args(filter: Expression | None) -> None:
|
|
|
25
25
|
raise TypeError("filter must be an Expression or None")
|
|
26
26
|
|
|
27
27
|
|
|
28
|
-
def validate_select_args(
|
|
29
|
-
|
|
28
|
+
def validate_select_args(
|
|
29
|
+
filter: Expression | None,
|
|
30
|
+
columns: Sequence[str] | None,
|
|
31
|
+
order_by: Sequence[str] | None = None,
|
|
32
|
+
descending: Sequence[bool] | None = None,
|
|
33
|
+
offset: int | None = None,
|
|
34
|
+
limit: int | None = None,
|
|
35
|
+
) -> None:
|
|
36
|
+
"""Validate selection, ordering, and pagination before opening a connection."""
|
|
30
37
|
if filter is not None and not isinstance(filter, Expression):
|
|
31
38
|
raise TypeError("filter must be an Expression or None")
|
|
32
|
-
|
|
33
|
-
if
|
|
34
|
-
|
|
35
|
-
if not
|
|
36
|
-
raise
|
|
37
|
-
if
|
|
39
|
+
for name, names in (("columns", columns), ("order_by", order_by)):
|
|
40
|
+
if names is None:
|
|
41
|
+
continue
|
|
42
|
+
if isinstance(names, (str, bytes)) or not isinstance(names, Sequence):
|
|
43
|
+
raise TypeError(f"{name} must be a sequence of column names")
|
|
44
|
+
if not names:
|
|
45
|
+
raise ValueError(f"{name} must contain at least one column")
|
|
46
|
+
if any(not isinstance(column, str) for column in names):
|
|
38
47
|
raise TypeError("Column names must be strings")
|
|
39
|
-
if any(not column or "\x00" in column for column in
|
|
48
|
+
if any(not column or "\x00" in column for column in names):
|
|
40
49
|
raise ValueError("Column names must be nonempty and contain no NUL characters")
|
|
50
|
+
if descending is not None:
|
|
51
|
+
if isinstance(descending, (str, bytes)) or not isinstance(descending, Sequence):
|
|
52
|
+
raise TypeError("descending must be a sequence of booleans")
|
|
53
|
+
if any(not isinstance(direction, bool) for direction in descending):
|
|
54
|
+
raise TypeError("descending must contain only booleans")
|
|
55
|
+
if order_by is None:
|
|
56
|
+
raise ValueError("descending requires order_by")
|
|
57
|
+
if len(descending) != len(order_by):
|
|
58
|
+
raise ValueError("descending must have the same length as order_by")
|
|
59
|
+
for name, value in (("offset", offset), ("limit", limit)):
|
|
60
|
+
if value is not None:
|
|
61
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
62
|
+
raise TypeError(f"{name} must be a nonnegative integer or None")
|
|
63
|
+
if value < 0:
|
|
64
|
+
raise ValueError(f"{name} must be a nonnegative integer")
|
|
41
65
|
|
|
42
66
|
|
|
43
67
|
def validate_update_args(
|
|
@@ -134,6 +158,10 @@ class DbAdapter(ABC):
|
|
|
134
158
|
*,
|
|
135
159
|
filter: Expression | None = None,
|
|
136
160
|
columns: Sequence[str] | None = None,
|
|
161
|
+
order_by: Sequence[str] | None = None,
|
|
162
|
+
descending: Sequence[bool] | None = None,
|
|
163
|
+
offset: int | None = None,
|
|
164
|
+
limit: int | None = None,
|
|
137
165
|
) -> list[dict[str, Any]]:
|
|
138
166
|
"""Select rows from one table as dictionaries.
|
|
139
167
|
|
|
@@ -145,11 +173,23 @@ class DbAdapter(ABC):
|
|
|
145
173
|
Condition to match; None selects all rows.
|
|
146
174
|
columns : sequence of str, optional
|
|
147
175
|
Nonempty sequence of column names; None selects all columns.
|
|
176
|
+
order_by : sequence of str, optional
|
|
177
|
+
Nonempty sequence of column names in sorting priority order.
|
|
178
|
+
None applies no sorting.
|
|
179
|
+
descending : sequence of bool, optional
|
|
180
|
+
One flag per order_by column: True for descending, False for
|
|
181
|
+
ascending. Requires order_by. None uses SQL's default ascending order.
|
|
182
|
+
offset : int, optional
|
|
183
|
+
Nonnegative number of rows to skip; None applies no offset.
|
|
184
|
+
limit : int, optional
|
|
185
|
+
Nonnegative maximum number of rows; None applies no row limit.
|
|
186
|
+
Zero returns no rows.
|
|
148
187
|
|
|
149
188
|
Returns
|
|
150
189
|
-------
|
|
151
190
|
list of dict
|
|
152
|
-
Matching rows keyed by column name
|
|
191
|
+
Matching rows keyed by column name. Order is unspecified unless
|
|
192
|
+
order_by is provided.
|
|
153
193
|
"""
|
|
154
194
|
raise NotImplementedError("This adapter does not implement selection")
|
|
155
195
|
|
|
@@ -159,6 +199,10 @@ class DbAdapter(ABC):
|
|
|
159
199
|
*,
|
|
160
200
|
filter: Expression | None = None,
|
|
161
201
|
columns: Sequence[str] | None = None,
|
|
202
|
+
order_by: Sequence[str] | None = None,
|
|
203
|
+
descending: Sequence[bool] | None = None,
|
|
204
|
+
offset: int | None = None,
|
|
205
|
+
limit: int | None = None,
|
|
162
206
|
) -> pl.DataFrame:
|
|
163
207
|
"""Select rows from one table as a Polars DataFrame.
|
|
164
208
|
|
|
@@ -170,12 +214,23 @@ class DbAdapter(ABC):
|
|
|
170
214
|
Condition to match; None selects all rows.
|
|
171
215
|
columns : sequence of str, optional
|
|
172
216
|
Nonempty sequence of column names; None selects all columns.
|
|
217
|
+
order_by : sequence of str, optional
|
|
218
|
+
Nonempty sequence of column names in sorting priority order.
|
|
219
|
+
None applies no sorting.
|
|
220
|
+
descending : sequence of bool, optional
|
|
221
|
+
One flag per order_by column: True for descending, False for
|
|
222
|
+
ascending. Requires order_by. None uses SQL's default ascending order.
|
|
223
|
+
offset : int, optional
|
|
224
|
+
Nonnegative number of rows to skip; None applies no offset.
|
|
225
|
+
limit : int, optional
|
|
226
|
+
Nonnegative maximum number of rows; None applies no row limit.
|
|
227
|
+
Zero returns no rows.
|
|
173
228
|
|
|
174
229
|
Returns
|
|
175
230
|
-------
|
|
176
231
|
polars.DataFrame
|
|
177
|
-
Matching rows with types inferred from all rows
|
|
178
|
-
|
|
232
|
+
Matching rows with types inferred from all rows. Order is unspecified
|
|
233
|
+
unless order_by is provided. An empty result has no columns.
|
|
179
234
|
"""
|
|
180
235
|
raise NotImplementedError("This adapter does not implement selection")
|
|
181
236
|
|
|
@@ -20,6 +20,12 @@ class MysqlAdapter(SqlAdapter):
|
|
|
20
20
|
self._conn_kwargs.update(conn_kwargs)
|
|
21
21
|
super().__init__()
|
|
22
22
|
|
|
23
|
+
def _select_pagination_sql(self, offset: int | None, limit: int | None) -> tuple[str, list[int]]:
|
|
24
|
+
if offset is not None and limit is None:
|
|
25
|
+
# MySQL requires LIMIT with OFFSET; this value means all remaining rows.
|
|
26
|
+
return " LIMIT 18446744073709551615 OFFSET %s", [offset]
|
|
27
|
+
return super()._select_pagination_sql(offset, limit)
|
|
28
|
+
|
|
23
29
|
def _build_upsert_sql(
|
|
24
30
|
self,
|
|
25
31
|
table: str,
|
|
@@ -7,7 +7,6 @@ import polars as pl
|
|
|
7
7
|
|
|
8
8
|
from .adapter import (
|
|
9
9
|
DbAdapter,
|
|
10
|
-
QueryArgs,
|
|
11
10
|
validate_insert_args,
|
|
12
11
|
validate_delete_args,
|
|
13
12
|
validate_select_args,
|
|
@@ -77,14 +76,29 @@ class SqlAdapter(DbAdapter):
|
|
|
77
76
|
*,
|
|
78
77
|
filter: Expression | None = None,
|
|
79
78
|
columns: Sequence[str] | None = None,
|
|
79
|
+
order_by: Sequence[str] | None = None,
|
|
80
|
+
descending: Sequence[bool] | None = None,
|
|
81
|
+
offset: int | None = None,
|
|
82
|
+
limit: int | None = None,
|
|
80
83
|
) -> list[dict[str, Any]]:
|
|
81
|
-
validate_select_args(filter, columns)
|
|
84
|
+
validate_select_args(filter, columns, order_by, descending, offset, limit)
|
|
82
85
|
projection = "*" if columns is None else ", ".join(self._quote_identifier(column) for column in columns)
|
|
83
86
|
qry = f"SELECT {projection} FROM {self._table_identifier(table)}"
|
|
84
87
|
args: list[Any] = []
|
|
85
88
|
if filter is not None:
|
|
86
89
|
condition, args = filter.to_param_sql(quote_identifier=self._quote_identifier)
|
|
87
90
|
qry += f" WHERE {condition}"
|
|
91
|
+
if order_by is not None:
|
|
92
|
+
ordering = []
|
|
93
|
+
for index, column in enumerate(order_by):
|
|
94
|
+
term = self._quote_identifier(column)
|
|
95
|
+
if descending is not None:
|
|
96
|
+
term += " DESC" if descending[index] else " ASC"
|
|
97
|
+
ordering.append(term)
|
|
98
|
+
qry += " ORDER BY " + ", ".join(ordering)
|
|
99
|
+
pagination, pagination_args = self._select_pagination_sql(offset, limit)
|
|
100
|
+
qry += pagination
|
|
101
|
+
args.extend(pagination_args)
|
|
88
102
|
return self._fetch_data(qry, args)
|
|
89
103
|
|
|
90
104
|
def select_polars(
|
|
@@ -93,12 +107,30 @@ class SqlAdapter(DbAdapter):
|
|
|
93
107
|
*,
|
|
94
108
|
filter: Expression | None = None,
|
|
95
109
|
columns: Sequence[str] | None = None,
|
|
110
|
+
order_by: Sequence[str] | None = None,
|
|
111
|
+
descending: Sequence[bool] | None = None,
|
|
112
|
+
offset: int | None = None,
|
|
113
|
+
limit: int | None = None,
|
|
96
114
|
) -> pl.DataFrame:
|
|
97
115
|
return pl.DataFrame(
|
|
98
|
-
self.select_data(
|
|
116
|
+
self.select_data(
|
|
117
|
+
table, filter=filter, columns=columns, order_by=order_by,
|
|
118
|
+
descending=descending, offset=offset, limit=limit,
|
|
119
|
+
),
|
|
99
120
|
infer_schema_length=None,
|
|
100
121
|
)
|
|
101
122
|
|
|
123
|
+
def _select_pagination_sql(self, offset: int | None, limit: int | None) -> tuple[str, list[int]]:
|
|
124
|
+
clause = ""
|
|
125
|
+
args = []
|
|
126
|
+
if limit is not None:
|
|
127
|
+
clause += " LIMIT %s"
|
|
128
|
+
args.append(limit)
|
|
129
|
+
if offset is not None:
|
|
130
|
+
clause += " OFFSET %s"
|
|
131
|
+
args.append(offset)
|
|
132
|
+
return clause, args
|
|
133
|
+
|
|
102
134
|
def update_polars(
|
|
103
135
|
self,
|
|
104
136
|
table: str,
|
|
@@ -1,10 +1,15 @@
|
|
|
1
|
+
from abc import abstractmethod
|
|
1
2
|
from typing import Awaitable, List, Union
|
|
2
3
|
|
|
3
4
|
from ._context import RequestContext
|
|
4
5
|
from ._input import DateStrArgType, parse_date_str_arg
|
|
5
6
|
|
|
6
7
|
|
|
7
|
-
__all__ = [
|
|
8
|
+
__all__ = [
|
|
9
|
+
'CalendarWebClient',
|
|
10
|
+
'SyncCalenderWebClient',
|
|
11
|
+
'AsyncCalendarWebClient'
|
|
12
|
+
]
|
|
8
13
|
|
|
9
14
|
|
|
10
15
|
class CalendarWebClient:
|
|
@@ -115,3 +120,41 @@ class CalendarWebClient:
|
|
|
115
120
|
'end': parse_date_str_arg(end, False),
|
|
116
121
|
'calendar': calendar
|
|
117
122
|
})
|
|
123
|
+
|
|
124
|
+
@abstractmethod
|
|
125
|
+
def is_trading_day(self, date: DateStrArgType, calendar: str = 'XSHG') -> Union[bool, Awaitable[bool]]:
|
|
126
|
+
"""Check if the specified date is a trading day.
|
|
127
|
+
|
|
128
|
+
Parameters
|
|
129
|
+
----------
|
|
130
|
+
date : str
|
|
131
|
+
The date to check.
|
|
132
|
+
calendar : str, default: 'XSHG'
|
|
133
|
+
|
|
134
|
+
Returns
|
|
135
|
+
----------
|
|
136
|
+
bool
|
|
137
|
+
True if the date is a trading day, False otherwise.
|
|
138
|
+
|
|
139
|
+
Examples
|
|
140
|
+
----------
|
|
141
|
+
>>> import yjdata
|
|
142
|
+
>>> cli = yjdata.WebDataClient()
|
|
143
|
+
>>> cli.calendar.is_trading_day('2025-07-18')
|
|
144
|
+
True
|
|
145
|
+
>>> cli.calendar.is_trading_day('2025-07-19')
|
|
146
|
+
False
|
|
147
|
+
"""
|
|
148
|
+
pass
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class SyncCalenderWebClient(CalendarWebClient):
|
|
152
|
+
def is_trading_day(self, date: DateStrArgType, calendar: str = 'XSHG') -> bool:
|
|
153
|
+
trading_days = self.range(date, date, calendar)
|
|
154
|
+
return len(trading_days) > 0
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class AsyncCalendarWebClient(CalendarWebClient):
|
|
158
|
+
async def is_trading_day(self, date: DateStrArgType, calendar: str = 'XSHG') -> bool:
|
|
159
|
+
trading_days = await self.range(date, date, calendar)
|
|
160
|
+
return len(trading_days) > 0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import os
|
|
2
2
|
|
|
3
|
-
from .calendar import
|
|
3
|
+
from .calendar import SyncCalenderWebClient, AsyncCalendarWebClient
|
|
4
4
|
from ._context import RequestContext, SyncRequestContext, AsyncRequestContext
|
|
5
5
|
|
|
6
6
|
|
|
@@ -23,11 +23,6 @@ class _WebClient:
|
|
|
23
23
|
self._host = host
|
|
24
24
|
self._context = context
|
|
25
25
|
|
|
26
|
-
@property
|
|
27
|
-
def calendar(self) -> CalendarWebClient:
|
|
28
|
-
"""API set for calendar services."""
|
|
29
|
-
return CalendarWebClient(self._host, self._context)
|
|
30
|
-
|
|
31
26
|
|
|
32
27
|
class WebDataClient(_WebClient):
|
|
33
28
|
"""Client for accessing YJData web API."""
|
|
@@ -35,9 +30,19 @@ class WebDataClient(_WebClient):
|
|
|
35
30
|
def __init__(self, host: str = _YJDATA_WEB_BASE_URL):
|
|
36
31
|
super().__init__(host, SyncRequestContext())
|
|
37
32
|
|
|
33
|
+
@property
|
|
34
|
+
def calendar(self) -> SyncCalenderWebClient:
|
|
35
|
+
"""API set for calendar services."""
|
|
36
|
+
return SyncCalenderWebClient(self._host, self._context)
|
|
37
|
+
|
|
38
38
|
|
|
39
39
|
class AsyncWebDataClient(_WebClient):
|
|
40
40
|
"""Async client for accessing YJData web API."""
|
|
41
41
|
|
|
42
42
|
def __init__(self, host: str = _YJDATA_WEB_BASE_URL):
|
|
43
43
|
super().__init__(host, AsyncRequestContext())
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def calendar(self) -> AsyncCalendarWebClient:
|
|
47
|
+
"""API set for calendar services."""
|
|
48
|
+
return AsyncCalendarWebClient(self._host, self._context)
|
|
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
|