yjdata 0.2.3__tar.gz → 0.2.4__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.2.4/PKG-INFO +66 -0
- yjdata-0.2.4/README.md +55 -0
- {yjdata-0.2.3 → yjdata-0.2.4}/pyproject.toml +1 -1
- {yjdata-0.2.3 → yjdata-0.2.4}/src/yjdata/web/calendar.py +3 -3
- {yjdata-0.2.3 → yjdata-0.2.4}/src/yjdata/web/client.py +12 -0
- yjdata-0.2.4/src/yjdata/web/cn_future.py +110 -0
- yjdata-0.2.4/src/yjdata/web/cn_stock.py +108 -0
- yjdata-0.2.4/src/yjdata.egg-info/PKG-INFO +66 -0
- {yjdata-0.2.3 → yjdata-0.2.4}/src/yjdata.egg-info/SOURCES.txt +2 -0
- yjdata-0.2.3/PKG-INFO +0 -130
- yjdata-0.2.3/README.md +0 -119
- yjdata-0.2.3/src/yjdata.egg-info/PKG-INFO +0 -130
- {yjdata-0.2.3 → yjdata-0.2.4}/setup.cfg +0 -0
- {yjdata-0.2.3 → yjdata-0.2.4}/src/yjdata/__init__.py +0 -0
- {yjdata-0.2.3 → yjdata-0.2.4}/src/yjdata/_optional.py +0 -0
- {yjdata-0.2.3 → yjdata-0.2.4}/src/yjdata/dataset/__init__.py +0 -0
- {yjdata-0.2.3 → yjdata-0.2.4}/src/yjdata/dataset/common.py +0 -0
- {yjdata-0.2.3 → yjdata-0.2.4}/src/yjdata/dataset/quote.py +0 -0
- {yjdata-0.2.3 → yjdata-0.2.4}/src/yjdata/dataset/snapshot.py +0 -0
- {yjdata-0.2.3 → yjdata-0.2.4}/src/yjdata/dataset/trade.py +0 -0
- {yjdata-0.2.3 → yjdata-0.2.4}/src/yjdata/db/__init__.py +0 -0
- {yjdata-0.2.3 → yjdata-0.2.4}/src/yjdata/db/adapter.py +0 -0
- {yjdata-0.2.3 → yjdata-0.2.4}/src/yjdata/db/expression.py +0 -0
- {yjdata-0.2.3 → yjdata-0.2.4}/src/yjdata/db/mysql.py +0 -0
- {yjdata-0.2.3 → yjdata-0.2.4}/src/yjdata/db/postgres.py +0 -0
- {yjdata-0.2.3 → yjdata-0.2.4}/src/yjdata/db/sql.py +0 -0
- {yjdata-0.2.3 → yjdata-0.2.4}/src/yjdata/dtype.py +0 -0
- {yjdata-0.2.3 → yjdata-0.2.4}/src/yjdata/utlity/__init__.py +0 -0
- {yjdata-0.2.3 → yjdata-0.2.4}/src/yjdata/utlity/s3.py +0 -0
- {yjdata-0.2.3 → yjdata-0.2.4}/src/yjdata/web/__init__.py +0 -0
- {yjdata-0.2.3 → yjdata-0.2.4}/src/yjdata/web/_context.py +0 -0
- {yjdata-0.2.3 → yjdata-0.2.4}/src/yjdata/web/_input.py +0 -0
- {yjdata-0.2.3 → yjdata-0.2.4}/src/yjdata/web/time_util.py +0 -0
- {yjdata-0.2.3 → yjdata-0.2.4}/src/yjdata.egg-info/dependency_links.txt +0 -0
- {yjdata-0.2.3 → yjdata-0.2.4}/src/yjdata.egg-info/requires.txt +0 -0
- {yjdata-0.2.3 → yjdata-0.2.4}/src/yjdata.egg-info/top_level.txt +0 -0
- {yjdata-0.2.3 → yjdata-0.2.4}/tests/test_batch_writes.py +0 -0
- {yjdata-0.2.3 → yjdata-0.2.4}/tests/test_last_of_group.py +0 -0
- {yjdata-0.2.3 → yjdata-0.2.4}/tests/test_mysql_batch_updates.py +0 -0
yjdata-0.2.4/PKG-INFO
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: yjdata
|
|
3
|
+
Version: 0.2.4
|
|
4
|
+
Summary: yjdata SDK for Python
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: httpx>=0.25
|
|
8
|
+
Requires-Dist: polars>=1.30
|
|
9
|
+
Requires-Dist: pyarrow>=18
|
|
10
|
+
Requires-Dist: pydantic>=2.11
|
|
11
|
+
|
|
12
|
+
# yjdata
|
|
13
|
+
|
|
14
|
+
Python SDK for the YJData web API, MySQL/PostgreSQL adapters, and Parquet utilities.
|
|
15
|
+
|
|
16
|
+
## Web API
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from yjdata import WebDataClient
|
|
20
|
+
|
|
21
|
+
client = WebDataClient()
|
|
22
|
+
prices = client.cn_stock.stk_eod_md("2026-11-06", ids="000001.XSHE")
|
|
23
|
+
for price in prices:
|
|
24
|
+
print(price.id, price.close)
|
|
25
|
+
|
|
26
|
+
trading_days = client.calendar.range("2026-11-01", "2026-11-06")
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
| Method | Data |
|
|
30
|
+
| --- | --- |
|
|
31
|
+
| `cn_stock.stk_eod_md(date, ids=None, stable_id=True)` | Stock end-of-day prices |
|
|
32
|
+
| `cn_stock.stk_sod_ref(date, ids=None)` | Stock pre-market reference data |
|
|
33
|
+
| `cn_future.fut_eod_md(date, ids=None)` | Futures end-of-day prices |
|
|
34
|
+
| `cn_future.fut_sod_ref(date, ids=None)` | Futures pre-market reference data |
|
|
35
|
+
| `calendar.next_n(start, n)`, `last_n(end, n)`, `range(start, end)`, `is_trading_day(date)` | Trading calendar; optional `calendar="XSHG"` |
|
|
36
|
+
|
|
37
|
+
- Dates accept `YYYY-MM-DD`, `YYYYMMDD`, `datetime.date`, or `datetime.datetime`.
|
|
38
|
+
- IDs accept lists or comma-separated strings. `None` returns all records;
|
|
39
|
+
`[]` or a blank string returns none.
|
|
40
|
+
- Stock prices use first-listing IDs by default. `stable_id=False` uses the
|
|
41
|
+
ticker on the requested date.
|
|
42
|
+
- Market data returns Pydantic models with friendly fields such as `close`,
|
|
43
|
+
`volume`, and `display_id`; `time` is a `YYYY-MM-DD` string. Models accept
|
|
44
|
+
friendly names and API aliases. `model_dump(by_alias=True)` exports API names.
|
|
45
|
+
- Use `AsyncWebDataClient` with `await` for the same methods.
|
|
46
|
+
|
|
47
|
+
See the [OpenAPI schema](https://yjdata-api.yaojingriver.internal/openapi.json)
|
|
48
|
+
for endpoint and field details.
|
|
49
|
+
|
|
50
|
+
## Database adapters
|
|
51
|
+
|
|
52
|
+
`MysqlAdapter` (MySQL 8.0+) and `PostgresAdapter` support
|
|
53
|
+
`select_last_of_group(table, group_by=..., sort_by=...)` and
|
|
54
|
+
`select_last_of_group_polars(...)`, returning dictionaries or a Polars DataFrame.
|
|
55
|
+
|
|
56
|
+
These methods select the last complete row per group in SQL. Use `ascending`
|
|
57
|
+
to control sort direction, `filter` before selection, `post_filter` afterward,
|
|
58
|
+
and `columns` for projection. NULLs sort below non-NULL values; include a unique
|
|
59
|
+
tie-breaker in `sort_by` for deterministic selection. Group output order is
|
|
60
|
+
unspecified.
|
|
61
|
+
|
|
62
|
+
## Tests
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
PYTHONPATH=src python -m unittest discover -s tests -v
|
|
66
|
+
```
|
yjdata-0.2.4/README.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# yjdata
|
|
2
|
+
|
|
3
|
+
Python SDK for the YJData web API, MySQL/PostgreSQL adapters, and Parquet utilities.
|
|
4
|
+
|
|
5
|
+
## Web API
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
from yjdata import WebDataClient
|
|
9
|
+
|
|
10
|
+
client = WebDataClient()
|
|
11
|
+
prices = client.cn_stock.stk_eod_md("2026-11-06", ids="000001.XSHE")
|
|
12
|
+
for price in prices:
|
|
13
|
+
print(price.id, price.close)
|
|
14
|
+
|
|
15
|
+
trading_days = client.calendar.range("2026-11-01", "2026-11-06")
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
| Method | Data |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| `cn_stock.stk_eod_md(date, ids=None, stable_id=True)` | Stock end-of-day prices |
|
|
21
|
+
| `cn_stock.stk_sod_ref(date, ids=None)` | Stock pre-market reference data |
|
|
22
|
+
| `cn_future.fut_eod_md(date, ids=None)` | Futures end-of-day prices |
|
|
23
|
+
| `cn_future.fut_sod_ref(date, ids=None)` | Futures pre-market reference data |
|
|
24
|
+
| `calendar.next_n(start, n)`, `last_n(end, n)`, `range(start, end)`, `is_trading_day(date)` | Trading calendar; optional `calendar="XSHG"` |
|
|
25
|
+
|
|
26
|
+
- Dates accept `YYYY-MM-DD`, `YYYYMMDD`, `datetime.date`, or `datetime.datetime`.
|
|
27
|
+
- IDs accept lists or comma-separated strings. `None` returns all records;
|
|
28
|
+
`[]` or a blank string returns none.
|
|
29
|
+
- Stock prices use first-listing IDs by default. `stable_id=False` uses the
|
|
30
|
+
ticker on the requested date.
|
|
31
|
+
- Market data returns Pydantic models with friendly fields such as `close`,
|
|
32
|
+
`volume`, and `display_id`; `time` is a `YYYY-MM-DD` string. Models accept
|
|
33
|
+
friendly names and API aliases. `model_dump(by_alias=True)` exports API names.
|
|
34
|
+
- Use `AsyncWebDataClient` with `await` for the same methods.
|
|
35
|
+
|
|
36
|
+
See the [OpenAPI schema](https://yjdata-api.yaojingriver.internal/openapi.json)
|
|
37
|
+
for endpoint and field details.
|
|
38
|
+
|
|
39
|
+
## Database adapters
|
|
40
|
+
|
|
41
|
+
`MysqlAdapter` (MySQL 8.0+) and `PostgresAdapter` support
|
|
42
|
+
`select_last_of_group(table, group_by=..., sort_by=...)` and
|
|
43
|
+
`select_last_of_group_polars(...)`, returning dictionaries or a Polars DataFrame.
|
|
44
|
+
|
|
45
|
+
These methods select the last complete row per group in SQL. Use `ascending`
|
|
46
|
+
to control sort direction, `filter` before selection, `post_filter` afterward,
|
|
47
|
+
and `columns` for projection. NULLs sort below non-NULL values; include a unique
|
|
48
|
+
tie-breaker in `sort_by` for deterministic selection. Group output order is
|
|
49
|
+
unspecified.
|
|
50
|
+
|
|
51
|
+
## Tests
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
PYTHONPATH=src python -m unittest discover -s tests -v
|
|
55
|
+
```
|
|
@@ -49,7 +49,7 @@ class CalendarWebClient:
|
|
|
49
49
|
return self._context.post(f'{self._api_root}/next-n', payload={
|
|
50
50
|
'start': parse_date_str_arg(start, False),
|
|
51
51
|
'n': n,
|
|
52
|
-
'
|
|
52
|
+
'exchange': calendar
|
|
53
53
|
})
|
|
54
54
|
|
|
55
55
|
def last_n(
|
|
@@ -83,7 +83,7 @@ class CalendarWebClient:
|
|
|
83
83
|
return self._context.post(f'{self._api_root}/last-n', payload={
|
|
84
84
|
'end': parse_date_str_arg(end, False),
|
|
85
85
|
'n': n,
|
|
86
|
-
'
|
|
86
|
+
'exchange': calendar
|
|
87
87
|
})
|
|
88
88
|
|
|
89
89
|
def range(
|
|
@@ -118,7 +118,7 @@ class CalendarWebClient:
|
|
|
118
118
|
return self._context.post(f'{self._api_root}/range', payload={
|
|
119
119
|
'start': parse_date_str_arg(start, False),
|
|
120
120
|
'end': parse_date_str_arg(end, False),
|
|
121
|
-
'
|
|
121
|
+
'exchange': calendar
|
|
122
122
|
})
|
|
123
123
|
|
|
124
124
|
@abstractmethod
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import os
|
|
2
2
|
|
|
3
3
|
from .calendar import SyncCalenderWebClient, AsyncCalendarWebClient
|
|
4
|
+
from .cn_future import CnFutureWebClient
|
|
5
|
+
from .cn_stock import CnStockWebClient
|
|
4
6
|
from ._context import RequestContext, SyncRequestContext, AsyncRequestContext
|
|
5
7
|
|
|
6
8
|
|
|
@@ -23,6 +25,16 @@ class _WebClient:
|
|
|
23
25
|
self._host = host
|
|
24
26
|
self._context = context
|
|
25
27
|
|
|
28
|
+
@property
|
|
29
|
+
def cn_stock(self) -> CnStockWebClient:
|
|
30
|
+
"""API set for domestic stock market data."""
|
|
31
|
+
return CnStockWebClient(self._host, self._context)
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def cn_future(self) -> CnFutureWebClient:
|
|
35
|
+
"""API set for domestic futures market data."""
|
|
36
|
+
return CnFutureWebClient(self._host, self._context)
|
|
37
|
+
|
|
26
38
|
|
|
27
39
|
class WebDataClient(_WebClient):
|
|
28
40
|
"""Client for accessing YJData web API."""
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from typing import Awaitable, List, Union
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
4
|
+
|
|
5
|
+
from ._context import RequestContext
|
|
6
|
+
from ._input import DateStrArgType, StrListArgType, parse_date_str_arg, parse_str_list_arg
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
__all__ = ['CnFutureWebClient', 'FutEodMd', 'FutSodRef']
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class FutEodMd(BaseModel):
|
|
13
|
+
"""Futures end-of-day prices, volume, turnover and open interest."""
|
|
14
|
+
|
|
15
|
+
model_config = ConfigDict(validate_by_name=True)
|
|
16
|
+
|
|
17
|
+
id: str
|
|
18
|
+
time: str = Field(alias='date')
|
|
19
|
+
pre_settle: float = Field(alias='ps')
|
|
20
|
+
pre_close: float = Field(alias='pc')
|
|
21
|
+
open: float = Field(alias='o')
|
|
22
|
+
high: float = Field(alias='h')
|
|
23
|
+
low: float = Field(alias='l')
|
|
24
|
+
close: float = Field(alias='c')
|
|
25
|
+
settle: float = Field(alias='s')
|
|
26
|
+
volume: int = Field(alias='vol')
|
|
27
|
+
amount: float = Field(alias='amt')
|
|
28
|
+
open_interest: int = Field(alias='oi')
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class FutSodRef(BaseModel):
|
|
32
|
+
"""Futures pre-market reference data, including contract specifications."""
|
|
33
|
+
|
|
34
|
+
model_config = ConfigDict(validate_by_name=True)
|
|
35
|
+
|
|
36
|
+
id: str
|
|
37
|
+
time: str = Field(alias='date')
|
|
38
|
+
name: str
|
|
39
|
+
display_id: str = Field(alias='dispId')
|
|
40
|
+
ticker: str
|
|
41
|
+
trading_status: str = Field(alias='status')
|
|
42
|
+
underlying_id: Union[str, None] = Field(alias='ulId')
|
|
43
|
+
product: str = Field(alias='prod')
|
|
44
|
+
multiplier: float = Field(alias='mult')
|
|
45
|
+
tick: float
|
|
46
|
+
pre_close: float = Field(alias='pc')
|
|
47
|
+
pre_settle: float = Field(alias='ps')
|
|
48
|
+
auct_hours: str = Field(alias='auct')
|
|
49
|
+
cont_hours: str = Field(alias='cont')
|
|
50
|
+
upper_limit: float = Field(alias='ul')
|
|
51
|
+
lower_limit: float = Field(alias='ll')
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class CnFutureWebClient:
|
|
55
|
+
|
|
56
|
+
def __init__(self, host: str, context: RequestContext):
|
|
57
|
+
self._api_root = f'http://{host}/v1/cn_future'
|
|
58
|
+
self._context = context
|
|
59
|
+
|
|
60
|
+
def fut_eod_md(
|
|
61
|
+
self,
|
|
62
|
+
date: DateStrArgType,
|
|
63
|
+
ids: StrListArgType = None
|
|
64
|
+
) -> Union[List[FutEodMd], Awaitable[List[FutEodMd]]]:
|
|
65
|
+
"""Get domestic futures end-of-day market data for a trading date.
|
|
66
|
+
|
|
67
|
+
Parameters
|
|
68
|
+
----------
|
|
69
|
+
date : str, datetime.date or datetime.datetime
|
|
70
|
+
Trading date; strings accept YYYY-MM-DD or YYYYMMDD.
|
|
71
|
+
ids : str or list[str], optional
|
|
72
|
+
IDs as a list or comma-separated string. None returns all records;
|
|
73
|
+
an empty list or blank string returns no records.
|
|
74
|
+
|
|
75
|
+
Returns
|
|
76
|
+
-------
|
|
77
|
+
list[FutEodMd] or Awaitable[list[FutEodMd]]
|
|
78
|
+
Validated records; await the result with AsyncWebDataClient.
|
|
79
|
+
"""
|
|
80
|
+
return self._context.post_model_list(FutEodMd, f'{self._api_root}/fut-eod-md', payload={
|
|
81
|
+
'date': parse_date_str_arg(date, False),
|
|
82
|
+
'ids': None if ids is None else parse_str_list_arg(ids)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
def fut_sod_ref(
|
|
86
|
+
self,
|
|
87
|
+
date: DateStrArgType,
|
|
88
|
+
ids: StrListArgType = None
|
|
89
|
+
) -> Union[List[FutSodRef], Awaitable[List[FutSodRef]]]:
|
|
90
|
+
"""Get domestic futures pre-market reference data for a trading date.
|
|
91
|
+
|
|
92
|
+
Parameters
|
|
93
|
+
----------
|
|
94
|
+
date : str, datetime.date or datetime.datetime
|
|
95
|
+
Trading date; strings accept YYYY-MM-DD or YYYYMMDD.
|
|
96
|
+
ids : str or list[str], optional
|
|
97
|
+
IDs as a list or comma-separated string. None returns all records;
|
|
98
|
+
an empty list or blank string returns no records.
|
|
99
|
+
|
|
100
|
+
Returns
|
|
101
|
+
-------
|
|
102
|
+
list[FutSodRef] or Awaitable[list[FutSodRef]]
|
|
103
|
+
Validated records; await the result with AsyncWebDataClient.
|
|
104
|
+
API fields dispId and ulId use display_id and underlying_id.
|
|
105
|
+
underlying_id is None for contracts without an underlying index.
|
|
106
|
+
"""
|
|
107
|
+
return self._context.post_model_list(FutSodRef, f'{self._api_root}/fut-sod-ref', payload={
|
|
108
|
+
'date': parse_date_str_arg(date, False),
|
|
109
|
+
'ids': None if ids is None else parse_str_list_arg(ids)
|
|
110
|
+
})
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
from typing import Awaitable, List, Union
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
4
|
+
|
|
5
|
+
from ._context import RequestContext
|
|
6
|
+
from ._input import DateStrArgType, StrListArgType, parse_date_str_arg, parse_str_list_arg
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
__all__ = ['CnStockWebClient', 'StkEodMd', 'StkSodRef']
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class StkEodMd(BaseModel):
|
|
13
|
+
"""Stock end-of-day prices, volume and turnover."""
|
|
14
|
+
|
|
15
|
+
model_config = ConfigDict(validate_by_name=True)
|
|
16
|
+
|
|
17
|
+
id: str
|
|
18
|
+
time: str = Field(alias='date')
|
|
19
|
+
pre_close: float = Field(alias='pc')
|
|
20
|
+
open: float = Field(alias='o')
|
|
21
|
+
high: float = Field(alias='h')
|
|
22
|
+
low: float = Field(alias='l')
|
|
23
|
+
close: float = Field(alias='c')
|
|
24
|
+
volume: int = Field(alias='vol')
|
|
25
|
+
amount: float = Field(alias='amt')
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class StkSodRef(BaseModel):
|
|
29
|
+
"""Stock pre-market reference data. IDs use the first listing ticker."""
|
|
30
|
+
|
|
31
|
+
model_config = ConfigDict(validate_by_name=True)
|
|
32
|
+
|
|
33
|
+
id: str
|
|
34
|
+
time: str = Field(alias='date')
|
|
35
|
+
name: str
|
|
36
|
+
display_id: str = Field(alias='dispId')
|
|
37
|
+
ticker: str
|
|
38
|
+
trading_status: str = Field(alias='status')
|
|
39
|
+
st_status: str = Field(alias='st')
|
|
40
|
+
pre_close: float = Field(alias='pc')
|
|
41
|
+
prior_close: float = Field(alias='prc')
|
|
42
|
+
upper_limit: float = Field(alias='ul')
|
|
43
|
+
lower_limit: float = Field(alias='ll')
|
|
44
|
+
auct_hours: str = Field(alias='auct')
|
|
45
|
+
cont_hours: str = Field(alias='cont')
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class CnStockWebClient:
|
|
49
|
+
|
|
50
|
+
def __init__(self, host: str, context: RequestContext):
|
|
51
|
+
self._api_root = f'http://{host}/v1/cn_stock'
|
|
52
|
+
self._context = context
|
|
53
|
+
|
|
54
|
+
def stk_eod_md(
|
|
55
|
+
self,
|
|
56
|
+
date: DateStrArgType,
|
|
57
|
+
ids: StrListArgType = None,
|
|
58
|
+
stable_id: bool = True
|
|
59
|
+
) -> Union[List[StkEodMd], Awaitable[List[StkEodMd]]]:
|
|
60
|
+
"""Get stock end-of-day market data for a trading date.
|
|
61
|
+
|
|
62
|
+
Parameters
|
|
63
|
+
----------
|
|
64
|
+
date : str, datetime.date or datetime.datetime
|
|
65
|
+
Trading date; strings accept YYYY-MM-DD or YYYYMMDD.
|
|
66
|
+
ids : str or list[str], optional
|
|
67
|
+
IDs as a list or comma-separated string. None returns all records;
|
|
68
|
+
an empty list or blank string returns no records.
|
|
69
|
+
stable_id : bool, default: True
|
|
70
|
+
Use the first listing ticker plus exchange for both the ID filter
|
|
71
|
+
and returned IDs. False uses the ticker on the requested date.
|
|
72
|
+
|
|
73
|
+
Returns
|
|
74
|
+
-------
|
|
75
|
+
list[StkEodMd] or Awaitable[list[StkEodMd]]
|
|
76
|
+
Validated records; await the result with AsyncWebDataClient.
|
|
77
|
+
"""
|
|
78
|
+
return self._context.post_model_list(StkEodMd, f'{self._api_root}/stk-eod-md', payload={
|
|
79
|
+
'date': parse_date_str_arg(date, False),
|
|
80
|
+
'ids': None if ids is None else parse_str_list_arg(ids),
|
|
81
|
+
'stableId': stable_id
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
def stk_sod_ref(
|
|
85
|
+
self,
|
|
86
|
+
date: DateStrArgType,
|
|
87
|
+
ids: StrListArgType = None
|
|
88
|
+
) -> Union[List[StkSodRef], Awaitable[List[StkSodRef]]]:
|
|
89
|
+
"""Get stock pre-market reference data for a trading date.
|
|
90
|
+
|
|
91
|
+
Parameters
|
|
92
|
+
----------
|
|
93
|
+
date : str, datetime.date or datetime.datetime
|
|
94
|
+
Trading date; strings accept YYYY-MM-DD or YYYYMMDD.
|
|
95
|
+
ids : str or list[str], optional
|
|
96
|
+
IDs as a list or comma-separated string. None returns all records;
|
|
97
|
+
an empty list or blank string returns no records.
|
|
98
|
+
|
|
99
|
+
Returns
|
|
100
|
+
-------
|
|
101
|
+
list[StkSodRef] or Awaitable[list[StkSodRef]]
|
|
102
|
+
Validated records; await the result with AsyncWebDataClient.
|
|
103
|
+
Fields use backend Python names, such as display_id for dispId.
|
|
104
|
+
"""
|
|
105
|
+
return self._context.post_model_list(StkSodRef, f'{self._api_root}/stk-sod-ref', payload={
|
|
106
|
+
'date': parse_date_str_arg(date, False),
|
|
107
|
+
'ids': None if ids is None else parse_str_list_arg(ids)
|
|
108
|
+
})
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: yjdata
|
|
3
|
+
Version: 0.2.4
|
|
4
|
+
Summary: yjdata SDK for Python
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: httpx>=0.25
|
|
8
|
+
Requires-Dist: polars>=1.30
|
|
9
|
+
Requires-Dist: pyarrow>=18
|
|
10
|
+
Requires-Dist: pydantic>=2.11
|
|
11
|
+
|
|
12
|
+
# yjdata
|
|
13
|
+
|
|
14
|
+
Python SDK for the YJData web API, MySQL/PostgreSQL adapters, and Parquet utilities.
|
|
15
|
+
|
|
16
|
+
## Web API
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from yjdata import WebDataClient
|
|
20
|
+
|
|
21
|
+
client = WebDataClient()
|
|
22
|
+
prices = client.cn_stock.stk_eod_md("2026-11-06", ids="000001.XSHE")
|
|
23
|
+
for price in prices:
|
|
24
|
+
print(price.id, price.close)
|
|
25
|
+
|
|
26
|
+
trading_days = client.calendar.range("2026-11-01", "2026-11-06")
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
| Method | Data |
|
|
30
|
+
| --- | --- |
|
|
31
|
+
| `cn_stock.stk_eod_md(date, ids=None, stable_id=True)` | Stock end-of-day prices |
|
|
32
|
+
| `cn_stock.stk_sod_ref(date, ids=None)` | Stock pre-market reference data |
|
|
33
|
+
| `cn_future.fut_eod_md(date, ids=None)` | Futures end-of-day prices |
|
|
34
|
+
| `cn_future.fut_sod_ref(date, ids=None)` | Futures pre-market reference data |
|
|
35
|
+
| `calendar.next_n(start, n)`, `last_n(end, n)`, `range(start, end)`, `is_trading_day(date)` | Trading calendar; optional `calendar="XSHG"` |
|
|
36
|
+
|
|
37
|
+
- Dates accept `YYYY-MM-DD`, `YYYYMMDD`, `datetime.date`, or `datetime.datetime`.
|
|
38
|
+
- IDs accept lists or comma-separated strings. `None` returns all records;
|
|
39
|
+
`[]` or a blank string returns none.
|
|
40
|
+
- Stock prices use first-listing IDs by default. `stable_id=False` uses the
|
|
41
|
+
ticker on the requested date.
|
|
42
|
+
- Market data returns Pydantic models with friendly fields such as `close`,
|
|
43
|
+
`volume`, and `display_id`; `time` is a `YYYY-MM-DD` string. Models accept
|
|
44
|
+
friendly names and API aliases. `model_dump(by_alias=True)` exports API names.
|
|
45
|
+
- Use `AsyncWebDataClient` with `await` for the same methods.
|
|
46
|
+
|
|
47
|
+
See the [OpenAPI schema](https://yjdata-api.yaojingriver.internal/openapi.json)
|
|
48
|
+
for endpoint and field details.
|
|
49
|
+
|
|
50
|
+
## Database adapters
|
|
51
|
+
|
|
52
|
+
`MysqlAdapter` (MySQL 8.0+) and `PostgresAdapter` support
|
|
53
|
+
`select_last_of_group(table, group_by=..., sort_by=...)` and
|
|
54
|
+
`select_last_of_group_polars(...)`, returning dictionaries or a Polars DataFrame.
|
|
55
|
+
|
|
56
|
+
These methods select the last complete row per group in SQL. Use `ascending`
|
|
57
|
+
to control sort direction, `filter` before selection, `post_filter` afterward,
|
|
58
|
+
and `columns` for projection. NULLs sort below non-NULL values; include a unique
|
|
59
|
+
tie-breaker in `sort_by` for deterministic selection. Group output order is
|
|
60
|
+
unspecified.
|
|
61
|
+
|
|
62
|
+
## Tests
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
PYTHONPATH=src python -m unittest discover -s tests -v
|
|
66
|
+
```
|
|
@@ -26,6 +26,8 @@ src/yjdata/web/_context.py
|
|
|
26
26
|
src/yjdata/web/_input.py
|
|
27
27
|
src/yjdata/web/calendar.py
|
|
28
28
|
src/yjdata/web/client.py
|
|
29
|
+
src/yjdata/web/cn_future.py
|
|
30
|
+
src/yjdata/web/cn_stock.py
|
|
29
31
|
src/yjdata/web/time_util.py
|
|
30
32
|
tests/test_batch_writes.py
|
|
31
33
|
tests/test_last_of_group.py
|
yjdata-0.2.3/PKG-INFO
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: yjdata
|
|
3
|
-
Version: 0.2.3
|
|
4
|
-
Summary: yjdata SDK for Python
|
|
5
|
-
Requires-Python: >=3.10
|
|
6
|
-
Description-Content-Type: text/markdown
|
|
7
|
-
Requires-Dist: httpx>=0.25
|
|
8
|
-
Requires-Dist: polars>=1.30
|
|
9
|
-
Requires-Dist: pyarrow>=18
|
|
10
|
-
Requires-Dist: pydantic>=2.11
|
|
11
|
-
|
|
12
|
-
# yjdata
|
|
13
|
-
|
|
14
|
-
Python SDK for yjdata, including MySQL and PostgreSQL database adapters.
|
|
15
|
-
|
|
16
|
-
## Select the last row of each group
|
|
17
|
-
|
|
18
|
-
`MysqlAdapter` (MySQL 8.0+) and `PostgresAdapter` provide:
|
|
19
|
-
|
|
20
|
-
- `select_last_of_group(...) -> list[dict[str, Any]]`
|
|
21
|
-
- `select_last_of_group_polars(...) -> polars.DataFrame`
|
|
22
|
-
|
|
23
|
-
Both methods execute grouping, row selection, filtering, and projection in SQL
|
|
24
|
-
using `ROW_NUMBER()`. They accept the table name followed by keyword arguments:
|
|
25
|
-
|
|
26
|
-
| Parameter | Meaning |
|
|
27
|
-
| --- | --- |
|
|
28
|
-
| `table` | Table name, optionally qualified by schema or database. |
|
|
29
|
-
| `group_by: Sequence[str]` | Required nonempty sequence of grouping columns. |
|
|
30
|
-
| `sort_by: Sequence[str]` | Required nonempty sequence of sort columns, in priority order. |
|
|
31
|
-
| `ascending: bool \| Sequence[bool] = True` | One direction for every sort column, or one boolean per column. |
|
|
32
|
-
| `columns: Sequence[str] \| None = None` | Nonempty sequence of output columns; `None` returns all original columns. |
|
|
33
|
-
| `filter: Expression \| None = None` | Filter source rows before choosing a row per group. |
|
|
34
|
-
| `post_filter: Expression \| None = None` | Filter the chosen rows, before projecting output columns. |
|
|
35
|
-
|
|
36
|
-
The operation first applies `filter`, orders each group, selects its last complete
|
|
37
|
-
source row, applies `post_filter`, and finally projects `columns`. Values come
|
|
38
|
-
from the same row, including NULL values; this is not a separate last-non-NULL
|
|
39
|
-
aggregation for each column. A post-filter rejection removes the group entirely,
|
|
40
|
-
without falling back to an older row.
|
|
41
|
-
|
|
42
|
-
NULLs sort below all non-NULL values on both backends: first for ascending order
|
|
43
|
-
and last for descending order. Consequently, ascending selection prefers the
|
|
44
|
-
largest non-NULL value, while descending selection prefers NULL when present.
|
|
45
|
-
This rule applies to each sort column. NULL grouping keys group together.
|
|
46
|
-
For deterministic selection among ties, include a unique column in `sort_by`,
|
|
47
|
-
such as `sort_by=["change_time", "record_id"]`. Output order across groups is
|
|
48
|
-
unspecified.
|
|
49
|
-
|
|
50
|
-
Group, sort, and filter columns need not appear in `columns`. Internal ranking
|
|
51
|
-
columns are excluded by the SQL projection, even if the table has similarly
|
|
52
|
-
named columns. With `columns=None`, an additional zero-row query reads column
|
|
53
|
-
metadata so the adapter can explicitly project all original columns.
|
|
54
|
-
An empty result is `[]` for dictionaries or a DataFrame with no columns for
|
|
55
|
-
Polars, matching the existing selection methods. Polars infers types from all
|
|
56
|
-
returned rows.
|
|
57
|
-
|
|
58
|
-
### THS concept snapshots
|
|
59
|
-
|
|
60
|
-
For an existing adapter `client` and a cutoff value `cutoff`:
|
|
61
|
-
|
|
62
|
-
```python
|
|
63
|
-
import yjdata.db.expression as ye
|
|
64
|
-
|
|
65
|
-
data = client.select_last_of_group_polars(
|
|
66
|
-
table="thsetl.concept_change_record",
|
|
67
|
-
group_by=["concept_id"],
|
|
68
|
-
sort_by=["change_time"],
|
|
69
|
-
ascending=True,
|
|
70
|
-
columns=["concept_id", "concept_name"],
|
|
71
|
-
filter=(
|
|
72
|
-
(ye.col("change_time") <= cutoff)
|
|
73
|
-
& (ye.col("isvalid") == "1")
|
|
74
|
-
),
|
|
75
|
-
post_filter=ye.col("change_type") != "conceptNegation",
|
|
76
|
-
)
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
This selects the latest valid change at or before the cutoff, then excludes
|
|
80
|
-
concepts whose selected change is a negation. The MySQL query has this form:
|
|
81
|
-
|
|
82
|
-
```sql
|
|
83
|
-
SELECT `concept_id`, `concept_name`
|
|
84
|
-
FROM (
|
|
85
|
-
SELECT `concept_id`, `concept_name`, `change_type`,
|
|
86
|
-
ROW_NUMBER() OVER (
|
|
87
|
-
PARTITION BY `concept_id`
|
|
88
|
-
ORDER BY (`change_time` IS NULL) ASC, `change_time` DESC
|
|
89
|
-
) AS `_yjdata_row_number`
|
|
90
|
-
FROM `thsetl`.`concept_change_record`
|
|
91
|
-
WHERE (`change_time` <= %s) AND (`isvalid` = %s)
|
|
92
|
-
) AS `_yjdata_latest`
|
|
93
|
-
WHERE `_yjdata_row_number` = 1 AND (`change_type` <> %s)
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
Values are bound in order: `[cutoff, "1", "conceptNegation"]`. PostgreSQL uses
|
|
97
|
-
the same query structure with double-quoted identifiers. Expressions retain
|
|
98
|
-
normal SQL NULL semantics: for example, `change_type <> 'conceptNegation'`
|
|
99
|
-
also excludes NULL `change_type` values.
|
|
100
|
-
|
|
101
|
-
## Batch writes
|
|
102
|
-
|
|
103
|
-
PostgreSQL inserts, upserts and updates use `psycopg2.extras.execute_batch`.
|
|
104
|
-
MySQL inserts and upserts use PyMySQL's optimized `executemany`. MySQL updates
|
|
105
|
-
combine driver-escaped UPDATE statements into one request, preserving input
|
|
106
|
-
order, repeated keys and ordinary `=` matching (NULL keys do not match).
|
|
107
|
-
MySQL connections enable `CLIENT.MULTI_STATEMENTS` for these update batches.
|
|
108
|
-
|
|
109
|
-
`batch_size` limits rows per batch. MySQL update batches also respect the
|
|
110
|
-
cursor's `max_stmt_length` in encoded bytes (normally about 1 MB); an individual
|
|
111
|
-
larger statement is sent alone and remains subject to the server's packet limit.
|
|
112
|
-
Every result is consumed so errors in later statements propagate before commit.
|
|
113
|
-
With the default transactional connection settings and transactional tables,
|
|
114
|
-
all batches commit together or roll back on failure. This reduces network
|
|
115
|
-
round trips; the server still executes one UPDATE per input row.
|
|
116
|
-
|
|
117
|
-
## Tests
|
|
118
|
-
|
|
119
|
-
```sh
|
|
120
|
-
python -m unittest discover -s tests -v
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
The grouped-selection tests exercise both adapters' validation, quoting,
|
|
124
|
-
parameter ordering, and result conversion. Generated queries execute against
|
|
125
|
-
SQLite's window engine with DB-API placeholders translated, so tests require
|
|
126
|
-
neither optional database drivers nor running servers. This does not replace
|
|
127
|
-
integration testing against MySQL and PostgreSQL.
|
|
128
|
-
|
|
129
|
-
With `pymysql` installed, the MySQL batch-update tests also exercise the real
|
|
130
|
-
driver's escaping, encoded request sizes and result handling without a server.
|
yjdata-0.2.3/README.md
DELETED
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
# yjdata
|
|
2
|
-
|
|
3
|
-
Python SDK for yjdata, including MySQL and PostgreSQL database adapters.
|
|
4
|
-
|
|
5
|
-
## Select the last row of each group
|
|
6
|
-
|
|
7
|
-
`MysqlAdapter` (MySQL 8.0+) and `PostgresAdapter` provide:
|
|
8
|
-
|
|
9
|
-
- `select_last_of_group(...) -> list[dict[str, Any]]`
|
|
10
|
-
- `select_last_of_group_polars(...) -> polars.DataFrame`
|
|
11
|
-
|
|
12
|
-
Both methods execute grouping, row selection, filtering, and projection in SQL
|
|
13
|
-
using `ROW_NUMBER()`. They accept the table name followed by keyword arguments:
|
|
14
|
-
|
|
15
|
-
| Parameter | Meaning |
|
|
16
|
-
| --- | --- |
|
|
17
|
-
| `table` | Table name, optionally qualified by schema or database. |
|
|
18
|
-
| `group_by: Sequence[str]` | Required nonempty sequence of grouping columns. |
|
|
19
|
-
| `sort_by: Sequence[str]` | Required nonempty sequence of sort columns, in priority order. |
|
|
20
|
-
| `ascending: bool \| Sequence[bool] = True` | One direction for every sort column, or one boolean per column. |
|
|
21
|
-
| `columns: Sequence[str] \| None = None` | Nonempty sequence of output columns; `None` returns all original columns. |
|
|
22
|
-
| `filter: Expression \| None = None` | Filter source rows before choosing a row per group. |
|
|
23
|
-
| `post_filter: Expression \| None = None` | Filter the chosen rows, before projecting output columns. |
|
|
24
|
-
|
|
25
|
-
The operation first applies `filter`, orders each group, selects its last complete
|
|
26
|
-
source row, applies `post_filter`, and finally projects `columns`. Values come
|
|
27
|
-
from the same row, including NULL values; this is not a separate last-non-NULL
|
|
28
|
-
aggregation for each column. A post-filter rejection removes the group entirely,
|
|
29
|
-
without falling back to an older row.
|
|
30
|
-
|
|
31
|
-
NULLs sort below all non-NULL values on both backends: first for ascending order
|
|
32
|
-
and last for descending order. Consequently, ascending selection prefers the
|
|
33
|
-
largest non-NULL value, while descending selection prefers NULL when present.
|
|
34
|
-
This rule applies to each sort column. NULL grouping keys group together.
|
|
35
|
-
For deterministic selection among ties, include a unique column in `sort_by`,
|
|
36
|
-
such as `sort_by=["change_time", "record_id"]`. Output order across groups is
|
|
37
|
-
unspecified.
|
|
38
|
-
|
|
39
|
-
Group, sort, and filter columns need not appear in `columns`. Internal ranking
|
|
40
|
-
columns are excluded by the SQL projection, even if the table has similarly
|
|
41
|
-
named columns. With `columns=None`, an additional zero-row query reads column
|
|
42
|
-
metadata so the adapter can explicitly project all original columns.
|
|
43
|
-
An empty result is `[]` for dictionaries or a DataFrame with no columns for
|
|
44
|
-
Polars, matching the existing selection methods. Polars infers types from all
|
|
45
|
-
returned rows.
|
|
46
|
-
|
|
47
|
-
### THS concept snapshots
|
|
48
|
-
|
|
49
|
-
For an existing adapter `client` and a cutoff value `cutoff`:
|
|
50
|
-
|
|
51
|
-
```python
|
|
52
|
-
import yjdata.db.expression as ye
|
|
53
|
-
|
|
54
|
-
data = client.select_last_of_group_polars(
|
|
55
|
-
table="thsetl.concept_change_record",
|
|
56
|
-
group_by=["concept_id"],
|
|
57
|
-
sort_by=["change_time"],
|
|
58
|
-
ascending=True,
|
|
59
|
-
columns=["concept_id", "concept_name"],
|
|
60
|
-
filter=(
|
|
61
|
-
(ye.col("change_time") <= cutoff)
|
|
62
|
-
& (ye.col("isvalid") == "1")
|
|
63
|
-
),
|
|
64
|
-
post_filter=ye.col("change_type") != "conceptNegation",
|
|
65
|
-
)
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
This selects the latest valid change at or before the cutoff, then excludes
|
|
69
|
-
concepts whose selected change is a negation. The MySQL query has this form:
|
|
70
|
-
|
|
71
|
-
```sql
|
|
72
|
-
SELECT `concept_id`, `concept_name`
|
|
73
|
-
FROM (
|
|
74
|
-
SELECT `concept_id`, `concept_name`, `change_type`,
|
|
75
|
-
ROW_NUMBER() OVER (
|
|
76
|
-
PARTITION BY `concept_id`
|
|
77
|
-
ORDER BY (`change_time` IS NULL) ASC, `change_time` DESC
|
|
78
|
-
) AS `_yjdata_row_number`
|
|
79
|
-
FROM `thsetl`.`concept_change_record`
|
|
80
|
-
WHERE (`change_time` <= %s) AND (`isvalid` = %s)
|
|
81
|
-
) AS `_yjdata_latest`
|
|
82
|
-
WHERE `_yjdata_row_number` = 1 AND (`change_type` <> %s)
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
Values are bound in order: `[cutoff, "1", "conceptNegation"]`. PostgreSQL uses
|
|
86
|
-
the same query structure with double-quoted identifiers. Expressions retain
|
|
87
|
-
normal SQL NULL semantics: for example, `change_type <> 'conceptNegation'`
|
|
88
|
-
also excludes NULL `change_type` values.
|
|
89
|
-
|
|
90
|
-
## Batch writes
|
|
91
|
-
|
|
92
|
-
PostgreSQL inserts, upserts and updates use `psycopg2.extras.execute_batch`.
|
|
93
|
-
MySQL inserts and upserts use PyMySQL's optimized `executemany`. MySQL updates
|
|
94
|
-
combine driver-escaped UPDATE statements into one request, preserving input
|
|
95
|
-
order, repeated keys and ordinary `=` matching (NULL keys do not match).
|
|
96
|
-
MySQL connections enable `CLIENT.MULTI_STATEMENTS` for these update batches.
|
|
97
|
-
|
|
98
|
-
`batch_size` limits rows per batch. MySQL update batches also respect the
|
|
99
|
-
cursor's `max_stmt_length` in encoded bytes (normally about 1 MB); an individual
|
|
100
|
-
larger statement is sent alone and remains subject to the server's packet limit.
|
|
101
|
-
Every result is consumed so errors in later statements propagate before commit.
|
|
102
|
-
With the default transactional connection settings and transactional tables,
|
|
103
|
-
all batches commit together or roll back on failure. This reduces network
|
|
104
|
-
round trips; the server still executes one UPDATE per input row.
|
|
105
|
-
|
|
106
|
-
## Tests
|
|
107
|
-
|
|
108
|
-
```sh
|
|
109
|
-
python -m unittest discover -s tests -v
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
The grouped-selection tests exercise both adapters' validation, quoting,
|
|
113
|
-
parameter ordering, and result conversion. Generated queries execute against
|
|
114
|
-
SQLite's window engine with DB-API placeholders translated, so tests require
|
|
115
|
-
neither optional database drivers nor running servers. This does not replace
|
|
116
|
-
integration testing against MySQL and PostgreSQL.
|
|
117
|
-
|
|
118
|
-
With `pymysql` installed, the MySQL batch-update tests also exercise the real
|
|
119
|
-
driver's escaping, encoded request sizes and result handling without a server.
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: yjdata
|
|
3
|
-
Version: 0.2.3
|
|
4
|
-
Summary: yjdata SDK for Python
|
|
5
|
-
Requires-Python: >=3.10
|
|
6
|
-
Description-Content-Type: text/markdown
|
|
7
|
-
Requires-Dist: httpx>=0.25
|
|
8
|
-
Requires-Dist: polars>=1.30
|
|
9
|
-
Requires-Dist: pyarrow>=18
|
|
10
|
-
Requires-Dist: pydantic>=2.11
|
|
11
|
-
|
|
12
|
-
# yjdata
|
|
13
|
-
|
|
14
|
-
Python SDK for yjdata, including MySQL and PostgreSQL database adapters.
|
|
15
|
-
|
|
16
|
-
## Select the last row of each group
|
|
17
|
-
|
|
18
|
-
`MysqlAdapter` (MySQL 8.0+) and `PostgresAdapter` provide:
|
|
19
|
-
|
|
20
|
-
- `select_last_of_group(...) -> list[dict[str, Any]]`
|
|
21
|
-
- `select_last_of_group_polars(...) -> polars.DataFrame`
|
|
22
|
-
|
|
23
|
-
Both methods execute grouping, row selection, filtering, and projection in SQL
|
|
24
|
-
using `ROW_NUMBER()`. They accept the table name followed by keyword arguments:
|
|
25
|
-
|
|
26
|
-
| Parameter | Meaning |
|
|
27
|
-
| --- | --- |
|
|
28
|
-
| `table` | Table name, optionally qualified by schema or database. |
|
|
29
|
-
| `group_by: Sequence[str]` | Required nonempty sequence of grouping columns. |
|
|
30
|
-
| `sort_by: Sequence[str]` | Required nonempty sequence of sort columns, in priority order. |
|
|
31
|
-
| `ascending: bool \| Sequence[bool] = True` | One direction for every sort column, or one boolean per column. |
|
|
32
|
-
| `columns: Sequence[str] \| None = None` | Nonempty sequence of output columns; `None` returns all original columns. |
|
|
33
|
-
| `filter: Expression \| None = None` | Filter source rows before choosing a row per group. |
|
|
34
|
-
| `post_filter: Expression \| None = None` | Filter the chosen rows, before projecting output columns. |
|
|
35
|
-
|
|
36
|
-
The operation first applies `filter`, orders each group, selects its last complete
|
|
37
|
-
source row, applies `post_filter`, and finally projects `columns`. Values come
|
|
38
|
-
from the same row, including NULL values; this is not a separate last-non-NULL
|
|
39
|
-
aggregation for each column. A post-filter rejection removes the group entirely,
|
|
40
|
-
without falling back to an older row.
|
|
41
|
-
|
|
42
|
-
NULLs sort below all non-NULL values on both backends: first for ascending order
|
|
43
|
-
and last for descending order. Consequently, ascending selection prefers the
|
|
44
|
-
largest non-NULL value, while descending selection prefers NULL when present.
|
|
45
|
-
This rule applies to each sort column. NULL grouping keys group together.
|
|
46
|
-
For deterministic selection among ties, include a unique column in `sort_by`,
|
|
47
|
-
such as `sort_by=["change_time", "record_id"]`. Output order across groups is
|
|
48
|
-
unspecified.
|
|
49
|
-
|
|
50
|
-
Group, sort, and filter columns need not appear in `columns`. Internal ranking
|
|
51
|
-
columns are excluded by the SQL projection, even if the table has similarly
|
|
52
|
-
named columns. With `columns=None`, an additional zero-row query reads column
|
|
53
|
-
metadata so the adapter can explicitly project all original columns.
|
|
54
|
-
An empty result is `[]` for dictionaries or a DataFrame with no columns for
|
|
55
|
-
Polars, matching the existing selection methods. Polars infers types from all
|
|
56
|
-
returned rows.
|
|
57
|
-
|
|
58
|
-
### THS concept snapshots
|
|
59
|
-
|
|
60
|
-
For an existing adapter `client` and a cutoff value `cutoff`:
|
|
61
|
-
|
|
62
|
-
```python
|
|
63
|
-
import yjdata.db.expression as ye
|
|
64
|
-
|
|
65
|
-
data = client.select_last_of_group_polars(
|
|
66
|
-
table="thsetl.concept_change_record",
|
|
67
|
-
group_by=["concept_id"],
|
|
68
|
-
sort_by=["change_time"],
|
|
69
|
-
ascending=True,
|
|
70
|
-
columns=["concept_id", "concept_name"],
|
|
71
|
-
filter=(
|
|
72
|
-
(ye.col("change_time") <= cutoff)
|
|
73
|
-
& (ye.col("isvalid") == "1")
|
|
74
|
-
),
|
|
75
|
-
post_filter=ye.col("change_type") != "conceptNegation",
|
|
76
|
-
)
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
This selects the latest valid change at or before the cutoff, then excludes
|
|
80
|
-
concepts whose selected change is a negation. The MySQL query has this form:
|
|
81
|
-
|
|
82
|
-
```sql
|
|
83
|
-
SELECT `concept_id`, `concept_name`
|
|
84
|
-
FROM (
|
|
85
|
-
SELECT `concept_id`, `concept_name`, `change_type`,
|
|
86
|
-
ROW_NUMBER() OVER (
|
|
87
|
-
PARTITION BY `concept_id`
|
|
88
|
-
ORDER BY (`change_time` IS NULL) ASC, `change_time` DESC
|
|
89
|
-
) AS `_yjdata_row_number`
|
|
90
|
-
FROM `thsetl`.`concept_change_record`
|
|
91
|
-
WHERE (`change_time` <= %s) AND (`isvalid` = %s)
|
|
92
|
-
) AS `_yjdata_latest`
|
|
93
|
-
WHERE `_yjdata_row_number` = 1 AND (`change_type` <> %s)
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
Values are bound in order: `[cutoff, "1", "conceptNegation"]`. PostgreSQL uses
|
|
97
|
-
the same query structure with double-quoted identifiers. Expressions retain
|
|
98
|
-
normal SQL NULL semantics: for example, `change_type <> 'conceptNegation'`
|
|
99
|
-
also excludes NULL `change_type` values.
|
|
100
|
-
|
|
101
|
-
## Batch writes
|
|
102
|
-
|
|
103
|
-
PostgreSQL inserts, upserts and updates use `psycopg2.extras.execute_batch`.
|
|
104
|
-
MySQL inserts and upserts use PyMySQL's optimized `executemany`. MySQL updates
|
|
105
|
-
combine driver-escaped UPDATE statements into one request, preserving input
|
|
106
|
-
order, repeated keys and ordinary `=` matching (NULL keys do not match).
|
|
107
|
-
MySQL connections enable `CLIENT.MULTI_STATEMENTS` for these update batches.
|
|
108
|
-
|
|
109
|
-
`batch_size` limits rows per batch. MySQL update batches also respect the
|
|
110
|
-
cursor's `max_stmt_length` in encoded bytes (normally about 1 MB); an individual
|
|
111
|
-
larger statement is sent alone and remains subject to the server's packet limit.
|
|
112
|
-
Every result is consumed so errors in later statements propagate before commit.
|
|
113
|
-
With the default transactional connection settings and transactional tables,
|
|
114
|
-
all batches commit together or roll back on failure. This reduces network
|
|
115
|
-
round trips; the server still executes one UPDATE per input row.
|
|
116
|
-
|
|
117
|
-
## Tests
|
|
118
|
-
|
|
119
|
-
```sh
|
|
120
|
-
python -m unittest discover -s tests -v
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
The grouped-selection tests exercise both adapters' validation, quoting,
|
|
124
|
-
parameter ordering, and result conversion. Generated queries execute against
|
|
125
|
-
SQLite's window engine with DB-API placeholders translated, so tests require
|
|
126
|
-
neither optional database drivers nor running servers. This does not replace
|
|
127
|
-
integration testing against MySQL and PostgreSQL.
|
|
128
|
-
|
|
129
|
-
With `pymysql` installed, the MySQL batch-update tests also exercise the real
|
|
130
|
-
driver's escaping, encoded request sizes and result handling without a server.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|