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.
Files changed (30) hide show
  1. {yjdata-0.1.0 → yjdata-0.1.2}/PKG-INFO +1 -1
  2. {yjdata-0.1.0 → yjdata-0.1.2}/pyproject.toml +1 -1
  3. {yjdata-0.1.0 → yjdata-0.1.2}/src/yjdata/dataset/common.py +2 -2
  4. {yjdata-0.1.0 → yjdata-0.1.2}/src/yjdata/db/adapter.py +67 -12
  5. {yjdata-0.1.0 → yjdata-0.1.2}/src/yjdata/db/mysql.py +6 -0
  6. {yjdata-0.1.0 → yjdata-0.1.2}/src/yjdata/db/sql.py +35 -3
  7. {yjdata-0.1.0 → yjdata-0.1.2}/src/yjdata/web/calendar.py +44 -1
  8. {yjdata-0.1.0 → yjdata-0.1.2}/src/yjdata/web/client.py +11 -6
  9. {yjdata-0.1.0 → yjdata-0.1.2}/src/yjdata.egg-info/PKG-INFO +1 -1
  10. {yjdata-0.1.0 → yjdata-0.1.2}/src/yjdata.egg-info/SOURCES.txt +2 -1
  11. yjdata-0.1.2/test/test.py +4 -0
  12. {yjdata-0.1.0 → yjdata-0.1.2}/README.md +0 -0
  13. {yjdata-0.1.0 → yjdata-0.1.2}/setup.cfg +0 -0
  14. {yjdata-0.1.0 → yjdata-0.1.2}/src/yjdata/__init__.py +0 -0
  15. {yjdata-0.1.0 → yjdata-0.1.2}/src/yjdata/_optional.py +0 -0
  16. {yjdata-0.1.0 → yjdata-0.1.2}/src/yjdata/dataset/__init__.py +0 -0
  17. {yjdata-0.1.0 → yjdata-0.1.2}/src/yjdata/dataset/quote.py +0 -0
  18. {yjdata-0.1.0 → yjdata-0.1.2}/src/yjdata/dataset/snapshot.py +0 -0
  19. {yjdata-0.1.0 → yjdata-0.1.2}/src/yjdata/dataset/trade.py +0 -0
  20. {yjdata-0.1.0 → yjdata-0.1.2}/src/yjdata/db/__init__.py +0 -0
  21. {yjdata-0.1.0 → yjdata-0.1.2}/src/yjdata/db/expression.py +0 -0
  22. {yjdata-0.1.0 → yjdata-0.1.2}/src/yjdata/db/postgres.py +0 -0
  23. {yjdata-0.1.0 → yjdata-0.1.2}/src/yjdata/dtype.py +0 -0
  24. {yjdata-0.1.0 → yjdata-0.1.2}/src/yjdata/web/__init__.py +0 -0
  25. {yjdata-0.1.0 → yjdata-0.1.2}/src/yjdata/web/_context.py +0 -0
  26. {yjdata-0.1.0 → yjdata-0.1.2}/src/yjdata/web/_input.py +0 -0
  27. {yjdata-0.1.0 → yjdata-0.1.2}/src/yjdata/web/time_util.py +0 -0
  28. {yjdata-0.1.0 → yjdata-0.1.2}/src/yjdata.egg-info/dependency_links.txt +0 -0
  29. {yjdata-0.1.0 → yjdata-0.1.2}/src/yjdata.egg-info/requires.txt +0 -0
  30. {yjdata-0.1.0 → yjdata-0.1.2}/src/yjdata.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: yjdata
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: yjdata SDK for Python
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "yjdata"
7
- version = "0.1.0"
7
+ version = "0.1.2"
8
8
  description = "yjdata SDK for Python"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -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(filter: Expression | None, columns: Sequence[str] | None) -> None:
29
- """Require an optional expression and a nonempty sequence of column names."""
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
- if columns is not None:
33
- if isinstance(columns, (str, bytes)) or not isinstance(columns, Sequence):
34
- raise TypeError("columns must be a sequence of column names")
35
- if not columns:
36
- raise ValueError("columns must contain at least one column")
37
- if any(not isinstance(column, str) for column in columns):
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 columns):
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, without a guaranteed order.
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, without a
178
- guaranteed order. An empty result has no columns.
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(table, filter=filter, columns=columns),
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__ = ['CalendarWebClient']
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 CalendarWebClient
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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: yjdata
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: yjdata SDK for Python
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -24,4 +24,5 @@ src/yjdata/web/_context.py
24
24
  src/yjdata/web/_input.py
25
25
  src/yjdata/web/calendar.py
26
26
  src/yjdata/web/client.py
27
- src/yjdata/web/time_util.py
27
+ src/yjdata/web/time_util.py
28
+ test/test.py
@@ -0,0 +1,4 @@
1
+ import yjdata
2
+
3
+ cli = yjdata.WebDataClient()
4
+ print(cli.calendar.is_trading_day('2026-01-01'))
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes