okama-macro 0.1.0__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.
- okama_macro-0.1.0/PKG-INFO +151 -0
- okama_macro-0.1.0/README.md +129 -0
- okama_macro-0.1.0/okama_macro/__init__.py +11 -0
- okama_macro-0.1.0/okama_macro/_frame.py +14 -0
- okama_macro-0.1.0/okama_macro/_http.py +127 -0
- okama_macro-0.1.0/okama_macro/registry.py +282 -0
- okama_macro-0.1.0/okama_macro/sources/__init__.py +0 -0
- okama_macro-0.1.0/okama_macro/sources/bis.py +63 -0
- okama_macro-0.1.0/okama_macro/sources/boe/__init__.py +4 -0
- okama_macro-0.1.0/okama_macro/sources/boe/kr.py +15 -0
- okama_macro-0.1.0/okama_macro/sources/boe/request_data.py +51 -0
- okama_macro-0.1.0/okama_macro/sources/boi/__init__.py +6 -0
- okama_macro-0.1.0/okama_macro/sources/boi/gdp.py +18 -0
- okama_macro-0.1.0/okama_macro/sources/boi/infl.py +40 -0
- okama_macro-0.1.0/okama_macro/sources/boi/kr.py +17 -0
- okama_macro-0.1.0/okama_macro/sources/boi/request_data.py +47 -0
- okama_macro-0.1.0/okama_macro/sources/boi/settings.py +2 -0
- okama_macro-0.1.0/okama_macro/sources/censtatd.py +55 -0
- okama_macro-0.1.0/okama_macro/sources/cfets/__init__.py +4 -0
- okama_macro-0.1.0/okama_macro/sources/cfets/lpr.py +44 -0
- okama_macro-0.1.0/okama_macro/sources/cfets/request_data.py +33 -0
- okama_macro-0.1.0/okama_macro/sources/ecb/__init__.py +13 -0
- okama_macro-0.1.0/okama_macro/sources/ecb/gdp.py +19 -0
- okama_macro-0.1.0/okama_macro/sources/ecb/hicp.py +17 -0
- okama_macro-0.1.0/okama_macro/sources/ecb/kr.py +101 -0
- okama_macro-0.1.0/okama_macro/sources/ecb/request_data.py +50 -0
- okama_macro-0.1.0/okama_macro/sources/fred.py +68 -0
- okama_macro-0.1.0/okama_macro/sources/hkma.py +91 -0
- okama_macro-0.1.0/okama_macro/sources/mospi.py +189 -0
- okama_macro-0.1.0/okama_macro/sources/nbsc/__init__.py +162 -0
- okama_macro-0.1.0/okama_macro/sources/nbsc/auto_retail.py +29 -0
- okama_macro-0.1.0/okama_macro/sources/nbsc/codes.json +326 -0
- okama_macro-0.1.0/okama_macro/sources/nbsc/gdp.py +42 -0
- okama_macro-0.1.0/okama_macro/sources/nbsc/household.py +33 -0
- okama_macro-0.1.0/okama_macro/sources/nbsc/inflation.py +121 -0
- okama_macro-0.1.0/okama_macro/sources/nbsc/investment.py +225 -0
- okama_macro-0.1.0/okama_macro/sources/nbsc/land.py +32 -0
- okama_macro-0.1.0/okama_macro/sources/nbsc/money_supply.py +38 -0
- okama_macro-0.1.0/okama_macro/sources/nbsc/pmi.py +26 -0
- okama_macro-0.1.0/okama_macro/sources/nbsc/ppi.py +22 -0
- okama_macro-0.1.0/okama_macro/sources/nbsc/request_data.py +377 -0
- okama_macro-0.1.0/okama_macro/sources/nbsc/unemployment.py +14 -0
- okama_macro-0.1.0/okama_macro/sources/ons/__init__.py +5 -0
- okama_macro-0.1.0/okama_macro/sources/ons/gdp.py +26 -0
- okama_macro-0.1.0/okama_macro/sources/ons/infl.py +33 -0
- okama_macro-0.1.0/okama_macro/sources/ons/request_data.py +32 -0
- okama_macro-0.1.0/okama_macro/sources/rbi.py +49 -0
- okama_macro-0.1.0/pyproject.toml +37 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: okama-macro
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Macro-economic data-source clients (CPI, central-bank rates) for the okama project
|
|
5
|
+
License: MIT
|
|
6
|
+
Author: mbk-dev
|
|
7
|
+
Requires-Python: >=3.11,<4.0
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Requires-Dist: lxml
|
|
15
|
+
Requires-Dist: pandas
|
|
16
|
+
Requires-Dist: requests
|
|
17
|
+
Requires-Dist: urllib3
|
|
18
|
+
Project-URL: Homepage, https://github.com/mbk-dev/okama-macro
|
|
19
|
+
Project-URL: Repository, https://github.com/mbk-dev/okama-macro
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# okama-macro
|
|
23
|
+
|
|
24
|
+
Macro-economic data-source clients — CPI inflation and central-bank / policy
|
|
25
|
+
rates — for the [okama](https://github.com/mbk-dev/okama) project.
|
|
26
|
+
|
|
27
|
+
One package, one HTTP/DataFrame layer, one dependency. It consolidates the macro
|
|
28
|
+
source clients that used to live as separate per-source micro-repos and as
|
|
29
|
+
in-repo modules of okama-API, behind a single unified series API. Rationale and
|
|
30
|
+
migration history: [mbk-dev/okama-API#41](https://github.com/mbk-dev/okama-API/issues/41)
|
|
31
|
+
and its ADR 0001.
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install okama-macro
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Requires **Python ≥ 3.11**. Runs on both **pandas 2.x and 3.x**.
|
|
40
|
+
|
|
41
|
+
## Quick start
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
import okama_macro
|
|
45
|
+
|
|
46
|
+
okama_macro.list_series()
|
|
47
|
+
# ['CHN_LPR1.RATE', 'CHN_LPR5.RATE', 'CNY.INFL', 'EU_DFR.RATE',
|
|
48
|
+
# 'EU_MLR.RATE', 'EU_MRO.RATE', 'GBP.INFL', 'HKD.INFL', 'HK_BR.RATE',
|
|
49
|
+
# 'ILS.INFL', 'IND_RBI.RATE', 'INR.INFL', 'ISR_IR.RATE', 'UK_BR.RATE',
|
|
50
|
+
# 'USD.INFL', 'US_EFFR.RATE']
|
|
51
|
+
|
|
52
|
+
# A rate as a decimal fraction (3.62% -> 0.0362), observations only:
|
|
53
|
+
okama_macro.get('US_EFFR.RATE', first_date='2020-01-01')
|
|
54
|
+
|
|
55
|
+
# Monthly m/m inflation as a decimal fraction:
|
|
56
|
+
okama_macro.get('USD.INFL')
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## The contract
|
|
60
|
+
|
|
61
|
+
Every series returned by `get()` obeys the same contract, so callers never
|
|
62
|
+
special-case a source:
|
|
63
|
+
|
|
64
|
+
- **Decimal fractions** — m/m inflation `0.0042`, a rate `0.0525` (never percent,
|
|
65
|
+
never an index level).
|
|
66
|
+
- **CPI series** are monthly, stamped on the **first of the month**, derived from
|
|
67
|
+
the source's index via `pct_change()` (base-invariant).
|
|
68
|
+
- **Rate series** normally carry **observations only — no padding**. Forward-fill
|
|
69
|
+
to a daily grid on the consumer side if you need one. `UK_BR.RATE` is the
|
|
70
|
+
documented exception: the Bank of England source publishes change dates, and
|
|
71
|
+
the client safely forward-fills them into a daily series.
|
|
72
|
+
- **Ascending `DatetimeIndex`**, `float` dtype, and `Series.name == key`.
|
|
73
|
+
|
|
74
|
+
`get()` raises `ValueError` for an unknown key (listing the known ones);
|
|
75
|
+
`list_series()` returns the available keys, sorted.
|
|
76
|
+
|
|
77
|
+
## Available series
|
|
78
|
+
|
|
79
|
+
| Key | Series | Source module |
|
|
80
|
+
|-----|--------|---------------|
|
|
81
|
+
| `USD.INFL` | US CPI, m/m | `fred` (FRED `CPIAUCNS`) |
|
|
82
|
+
| `HKD.INFL` | Hong Kong Composite CPI, m/m | `censtatd` (HK C&SD) |
|
|
83
|
+
| `INR.INFL` | India General CPI, m/m | `mospi` (MOSPI) |
|
|
84
|
+
| `CNY.INFL` | China CPI, m/m | `nbsc` (NBS China) |
|
|
85
|
+
| `GBP.INFL` | UK CPIH, m/m | `ons` (UK ONS) |
|
|
86
|
+
| `ILS.INFL` | Israel CPI, m/m | `boi` (Bank of Israel) |
|
|
87
|
+
| `US_EFFR.RATE` | US Federal Funds rate | `fred` (FRED `DFF`) |
|
|
88
|
+
| `HK_BR.RATE` | HKMA Discount Window Base Rate | `hkma` |
|
|
89
|
+
| `IND_RBI.RATE` | RBI policy repo rate | `bis` (history) + `rbi` (same-day tail) |
|
|
90
|
+
| `CHN_LPR1.RATE` | China one-year Loan Prime Rate | `cfets` |
|
|
91
|
+
| `CHN_LPR5.RATE` | China five-year Loan Prime Rate | `cfets` |
|
|
92
|
+
| `ISR_IR.RATE` | Bank of Israel policy rate | `boi` |
|
|
93
|
+
| `EU_MRO.RATE` | ECB main refinancing operations rate | `ecb` |
|
|
94
|
+
| `EU_MLR.RATE` | ECB marginal lending facility rate | `ecb` |
|
|
95
|
+
| `EU_DFR.RATE` | ECB deposit facility rate | `ecb` |
|
|
96
|
+
| `UK_BR.RATE` | Bank of England Bank Rate | `boe` |
|
|
97
|
+
|
|
98
|
+
## Raw per-source clients
|
|
99
|
+
|
|
100
|
+
Each source also exposes its raw client under `okama_macro.sources.*`, returning
|
|
101
|
+
data **as the agency publishes it** (CPI index levels, rates in percent) — use
|
|
102
|
+
these only if you need the unnormalised series; prefer `get()` otherwise.
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from okama_macro.sources import (
|
|
106
|
+
bis, boe, boi, censtatd, cfets, ecb, fred, hkma, mospi, nbsc, ons, rbi
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
hkma.get_base_rate() # percent, daily
|
|
110
|
+
censtatd.get_composite_cpi() # CPI index level, monthly
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Configuration (environment)
|
|
114
|
+
|
|
115
|
+
| Variable | Needed for | Notes |
|
|
116
|
+
|----------|-----------|-------|
|
|
117
|
+
| `FRED_API_KEY` | `USD.INFL`, `US_EFFR.RATE` | Free key from FRED; kept out of logs. |
|
|
118
|
+
| `PROXY_HOST`, `PROXY_PORT` | `bis`, `mospi`, `rbi` | Optional outbound HTTP proxy. |
|
|
119
|
+
| `PROXY_USER`, `PROXY_PASS` | — | Optional proxy credentials. |
|
|
120
|
+
|
|
121
|
+
## Architecture
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
okama_macro/
|
|
125
|
+
├── __init__.py # public API: get(), list_series()
|
|
126
|
+
├── registry.py # key -> normalised Series (the contract)
|
|
127
|
+
├── _http.py # shared Session: retry/back-off, proxy, browser UA,
|
|
128
|
+
│ # legacy-TLS, secret redaction
|
|
129
|
+
├── _frame.py # DataFrame/Series shaping helpers
|
|
130
|
+
└── sources/ # one client per source (bis, boe, boi, censtatd, cfets,
|
|
131
|
+
# ecb, fred, hkma, mospi, nbsc, ons, rbi)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Two layers: thin **sources** (data as published, on the shared `_http`) and a
|
|
135
|
+
**registry** that normalises each key to the contract above.
|
|
136
|
+
|
|
137
|
+
## Development
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
poetry install
|
|
141
|
+
poetry run pytest # test suite
|
|
142
|
+
poetry run ruff check . # lint (rules C, E, F, W, B)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
CI runs the suite and lint on Python 3.11, 3.12, 3.13 and 3.14. `poetry.lock` is
|
|
146
|
+
intentionally untracked.
|
|
147
|
+
|
|
148
|
+
## License
|
|
149
|
+
|
|
150
|
+
MIT.
|
|
151
|
+
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# okama-macro
|
|
2
|
+
|
|
3
|
+
Macro-economic data-source clients — CPI inflation and central-bank / policy
|
|
4
|
+
rates — for the [okama](https://github.com/mbk-dev/okama) project.
|
|
5
|
+
|
|
6
|
+
One package, one HTTP/DataFrame layer, one dependency. It consolidates the macro
|
|
7
|
+
source clients that used to live as separate per-source micro-repos and as
|
|
8
|
+
in-repo modules of okama-API, behind a single unified series API. Rationale and
|
|
9
|
+
migration history: [mbk-dev/okama-API#41](https://github.com/mbk-dev/okama-API/issues/41)
|
|
10
|
+
and its ADR 0001.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install okama-macro
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Requires **Python ≥ 3.11**. Runs on both **pandas 2.x and 3.x**.
|
|
19
|
+
|
|
20
|
+
## Quick start
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
import okama_macro
|
|
24
|
+
|
|
25
|
+
okama_macro.list_series()
|
|
26
|
+
# ['CHN_LPR1.RATE', 'CHN_LPR5.RATE', 'CNY.INFL', 'EU_DFR.RATE',
|
|
27
|
+
# 'EU_MLR.RATE', 'EU_MRO.RATE', 'GBP.INFL', 'HKD.INFL', 'HK_BR.RATE',
|
|
28
|
+
# 'ILS.INFL', 'IND_RBI.RATE', 'INR.INFL', 'ISR_IR.RATE', 'UK_BR.RATE',
|
|
29
|
+
# 'USD.INFL', 'US_EFFR.RATE']
|
|
30
|
+
|
|
31
|
+
# A rate as a decimal fraction (3.62% -> 0.0362), observations only:
|
|
32
|
+
okama_macro.get('US_EFFR.RATE', first_date='2020-01-01')
|
|
33
|
+
|
|
34
|
+
# Monthly m/m inflation as a decimal fraction:
|
|
35
|
+
okama_macro.get('USD.INFL')
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## The contract
|
|
39
|
+
|
|
40
|
+
Every series returned by `get()` obeys the same contract, so callers never
|
|
41
|
+
special-case a source:
|
|
42
|
+
|
|
43
|
+
- **Decimal fractions** — m/m inflation `0.0042`, a rate `0.0525` (never percent,
|
|
44
|
+
never an index level).
|
|
45
|
+
- **CPI series** are monthly, stamped on the **first of the month**, derived from
|
|
46
|
+
the source's index via `pct_change()` (base-invariant).
|
|
47
|
+
- **Rate series** normally carry **observations only — no padding**. Forward-fill
|
|
48
|
+
to a daily grid on the consumer side if you need one. `UK_BR.RATE` is the
|
|
49
|
+
documented exception: the Bank of England source publishes change dates, and
|
|
50
|
+
the client safely forward-fills them into a daily series.
|
|
51
|
+
- **Ascending `DatetimeIndex`**, `float` dtype, and `Series.name == key`.
|
|
52
|
+
|
|
53
|
+
`get()` raises `ValueError` for an unknown key (listing the known ones);
|
|
54
|
+
`list_series()` returns the available keys, sorted.
|
|
55
|
+
|
|
56
|
+
## Available series
|
|
57
|
+
|
|
58
|
+
| Key | Series | Source module |
|
|
59
|
+
|-----|--------|---------------|
|
|
60
|
+
| `USD.INFL` | US CPI, m/m | `fred` (FRED `CPIAUCNS`) |
|
|
61
|
+
| `HKD.INFL` | Hong Kong Composite CPI, m/m | `censtatd` (HK C&SD) |
|
|
62
|
+
| `INR.INFL` | India General CPI, m/m | `mospi` (MOSPI) |
|
|
63
|
+
| `CNY.INFL` | China CPI, m/m | `nbsc` (NBS China) |
|
|
64
|
+
| `GBP.INFL` | UK CPIH, m/m | `ons` (UK ONS) |
|
|
65
|
+
| `ILS.INFL` | Israel CPI, m/m | `boi` (Bank of Israel) |
|
|
66
|
+
| `US_EFFR.RATE` | US Federal Funds rate | `fred` (FRED `DFF`) |
|
|
67
|
+
| `HK_BR.RATE` | HKMA Discount Window Base Rate | `hkma` |
|
|
68
|
+
| `IND_RBI.RATE` | RBI policy repo rate | `bis` (history) + `rbi` (same-day tail) |
|
|
69
|
+
| `CHN_LPR1.RATE` | China one-year Loan Prime Rate | `cfets` |
|
|
70
|
+
| `CHN_LPR5.RATE` | China five-year Loan Prime Rate | `cfets` |
|
|
71
|
+
| `ISR_IR.RATE` | Bank of Israel policy rate | `boi` |
|
|
72
|
+
| `EU_MRO.RATE` | ECB main refinancing operations rate | `ecb` |
|
|
73
|
+
| `EU_MLR.RATE` | ECB marginal lending facility rate | `ecb` |
|
|
74
|
+
| `EU_DFR.RATE` | ECB deposit facility rate | `ecb` |
|
|
75
|
+
| `UK_BR.RATE` | Bank of England Bank Rate | `boe` |
|
|
76
|
+
|
|
77
|
+
## Raw per-source clients
|
|
78
|
+
|
|
79
|
+
Each source also exposes its raw client under `okama_macro.sources.*`, returning
|
|
80
|
+
data **as the agency publishes it** (CPI index levels, rates in percent) — use
|
|
81
|
+
these only if you need the unnormalised series; prefer `get()` otherwise.
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from okama_macro.sources import (
|
|
85
|
+
bis, boe, boi, censtatd, cfets, ecb, fred, hkma, mospi, nbsc, ons, rbi
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
hkma.get_base_rate() # percent, daily
|
|
89
|
+
censtatd.get_composite_cpi() # CPI index level, monthly
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Configuration (environment)
|
|
93
|
+
|
|
94
|
+
| Variable | Needed for | Notes |
|
|
95
|
+
|----------|-----------|-------|
|
|
96
|
+
| `FRED_API_KEY` | `USD.INFL`, `US_EFFR.RATE` | Free key from FRED; kept out of logs. |
|
|
97
|
+
| `PROXY_HOST`, `PROXY_PORT` | `bis`, `mospi`, `rbi` | Optional outbound HTTP proxy. |
|
|
98
|
+
| `PROXY_USER`, `PROXY_PASS` | — | Optional proxy credentials. |
|
|
99
|
+
|
|
100
|
+
## Architecture
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
okama_macro/
|
|
104
|
+
├── __init__.py # public API: get(), list_series()
|
|
105
|
+
├── registry.py # key -> normalised Series (the contract)
|
|
106
|
+
├── _http.py # shared Session: retry/back-off, proxy, browser UA,
|
|
107
|
+
│ # legacy-TLS, secret redaction
|
|
108
|
+
├── _frame.py # DataFrame/Series shaping helpers
|
|
109
|
+
└── sources/ # one client per source (bis, boe, boi, censtatd, cfets,
|
|
110
|
+
# ecb, fred, hkma, mospi, nbsc, ons, rbi)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Two layers: thin **sources** (data as published, on the shared `_http`) and a
|
|
114
|
+
**registry** that normalises each key to the contract above.
|
|
115
|
+
|
|
116
|
+
## Development
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
poetry install
|
|
120
|
+
poetry run pytest # test suite
|
|
121
|
+
poetry run ruff check . # lint (rules C, E, F, W, B)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
CI runs the suite and lint on Python 3.11, 3.12, 3.13 and 3.14. `poetry.lock` is
|
|
125
|
+
intentionally untracked.
|
|
126
|
+
|
|
127
|
+
## License
|
|
128
|
+
|
|
129
|
+
MIT.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""okama-macro: consolidated macro-economic data-source clients for okama.
|
|
2
|
+
|
|
3
|
+
Public API: ``get(key, first_date=None, last_date=None)`` returns a normalised
|
|
4
|
+
``pd.Series`` (decimal fractions, ascending DatetimeIndex, observations only);
|
|
5
|
+
``list_series()`` lists the available keys. Per-source raw clients live in
|
|
6
|
+
``okama_macro.sources``.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from okama_macro.registry import get, list_series
|
|
10
|
+
|
|
11
|
+
__all__ = ['get', 'list_series']
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Shared DataFrame/Series shaping helpers."""
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def clip_window(obj: pd.Series | pd.DataFrame,
|
|
7
|
+
first_date: object = None,
|
|
8
|
+
last_date: object = None) -> pd.Series | pd.DataFrame:
|
|
9
|
+
"""Clip a DatetimeIndex-ed object to the inclusive [first_date, last_date]."""
|
|
10
|
+
if first_date is not None:
|
|
11
|
+
obj = obj[obj.index >= pd.Timestamp(first_date)]
|
|
12
|
+
if last_date is not None:
|
|
13
|
+
obj = obj[obj.index <= pd.Timestamp(last_date)]
|
|
14
|
+
return obj
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Shared HTTP plumbing for all okama-macro source clients.
|
|
2
|
+
|
|
3
|
+
``get()`` is a plain GET with a browser-like User-Agent (several statistical
|
|
4
|
+
agencies reject the default python-requests UA), an optional outbound proxy
|
|
5
|
+
taken from the ``PROXY_*`` environment variables, and linear-back-off retries
|
|
6
|
+
on transient upstream 5xx responses. 4xx responses and hard connection errors
|
|
7
|
+
fail fast. On failure it raises ``RuntimeError`` with the ``redact`` strings
|
|
8
|
+
masked so API keys never leak into logs. ``legacy_tls_session()`` (Task 3)
|
|
9
|
+
serves endpoints that need OpenSSL legacy server renegotiation.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
import os
|
|
14
|
+
import ssl
|
|
15
|
+
import time
|
|
16
|
+
|
|
17
|
+
import requests
|
|
18
|
+
import urllib3
|
|
19
|
+
from requests.adapters import HTTPAdapter
|
|
20
|
+
from urllib3 import PoolManager
|
|
21
|
+
|
|
22
|
+
info_logger = logging.getLogger('okama_macro.http')
|
|
23
|
+
|
|
24
|
+
DEFAULT_TIMEOUT = 60 # seconds
|
|
25
|
+
USER_AGENT = 'Mozilla/5.0 (okama-data pipeline)'
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def proxies_from_env() -> dict[str, str] | None:
|
|
29
|
+
"""Build the requests proxies dict from PROXY_* env vars, or None if unset.
|
|
30
|
+
|
|
31
|
+
Foreign sources go through the local HAProxy on the production server;
|
|
32
|
+
without the env vars (e.g. in tests) requests go direct.
|
|
33
|
+
"""
|
|
34
|
+
host, port = os.getenv('PROXY_HOST'), os.getenv('PROXY_PORT')
|
|
35
|
+
if not (host and port):
|
|
36
|
+
return None
|
|
37
|
+
user, password = os.getenv('PROXY_USER'), os.getenv('PROXY_PASS')
|
|
38
|
+
auth = f'{user}:{password}@' if user and password else ''
|
|
39
|
+
url = f'http://{auth}{host}:{port}'
|
|
40
|
+
return {'http': url, 'https': url}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class _LegacyRenegotiationAdapter(HTTPAdapter):
|
|
44
|
+
"""HTTPAdapter enabling OpenSSL legacy server connect (api.mospi.gov.in)."""
|
|
45
|
+
|
|
46
|
+
def __init__(self, ssl_context: ssl.SSLContext, **kwargs):
|
|
47
|
+
self._ssl_context = ssl_context
|
|
48
|
+
super().__init__(**kwargs)
|
|
49
|
+
|
|
50
|
+
def init_poolmanager(self, connections, maxsize, block=False, **pool_kwargs):
|
|
51
|
+
pool_kwargs['ssl_context'] = self._ssl_context
|
|
52
|
+
self.poolmanager = PoolManager(
|
|
53
|
+
num_pools=connections, maxsize=maxsize, block=block, **pool_kwargs,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def proxy_manager_for(self, proxy, **proxy_kwargs):
|
|
57
|
+
# Proxied HTTPS uses a separate ProxyManager that does NOT inherit the
|
|
58
|
+
# context from init_poolmanager, so inject it here too. On the
|
|
59
|
+
# production server MOSPI is fetched through the local HAProxy; without
|
|
60
|
+
# this the tunneled TLS handshake loses OP_LEGACY_SERVER_CONNECT and
|
|
61
|
+
# fails with UNSAFE_LEGACY_RENEGOTIATION_DISABLED.
|
|
62
|
+
proxy_kwargs['ssl_context'] = self._ssl_context
|
|
63
|
+
return super().proxy_manager_for(proxy, **proxy_kwargs)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get(url: str,
|
|
67
|
+
*,
|
|
68
|
+
params: dict | None = None,
|
|
69
|
+
headers: dict | None = None,
|
|
70
|
+
timeout: int = DEFAULT_TIMEOUT,
|
|
71
|
+
max_attempts: int = 3,
|
|
72
|
+
backoff: float = 1.0,
|
|
73
|
+
use_proxy: bool = False,
|
|
74
|
+
redact: tuple[str, ...] = (),
|
|
75
|
+
label: str = 'request') -> requests.Response:
|
|
76
|
+
"""GET ``url`` with UA/proxy defaults and linear-back-off retries on 5xx.
|
|
77
|
+
|
|
78
|
+
Caller-supplied ``headers`` are merged over the defaults, so an explicit
|
|
79
|
+
``User-Agent`` from the caller wins.
|
|
80
|
+
"""
|
|
81
|
+
merged_headers = {'User-Agent': USER_AGENT} | (headers or {})
|
|
82
|
+
proxies = proxies_from_env() if use_proxy else None
|
|
83
|
+
for attempt in range(max_attempts):
|
|
84
|
+
try:
|
|
85
|
+
response = requests.get(url, params=params, headers=merged_headers,
|
|
86
|
+
timeout=timeout, proxies=proxies)
|
|
87
|
+
response.raise_for_status()
|
|
88
|
+
return response
|
|
89
|
+
except requests.RequestException as error:
|
|
90
|
+
resp = getattr(error, 'response', None)
|
|
91
|
+
transient = resp is not None and resp.status_code >= 500
|
|
92
|
+
if transient and attempt < max_attempts - 1:
|
|
93
|
+
info_logger.warning(
|
|
94
|
+
f'{label}: HTTP {resp.status_code}; '
|
|
95
|
+
f'retry {attempt + 1}/{max_attempts - 1}'
|
|
96
|
+
)
|
|
97
|
+
time.sleep(backoff * (attempt + 1))
|
|
98
|
+
continue
|
|
99
|
+
message = str(error)
|
|
100
|
+
for secret in redact:
|
|
101
|
+
message = message.replace(secret, '***')
|
|
102
|
+
raise RuntimeError(f'{label} failed: {message}') from None
|
|
103
|
+
msg = f'{label} failed after {max_attempts} attempts'
|
|
104
|
+
raise RuntimeError(msg) # pragma: no cover
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def legacy_tls_session() -> requests.Session:
|
|
108
|
+
"""A Session for endpoints needing legacy TLS renegotiation (MOSPI).
|
|
109
|
+
|
|
110
|
+
Verification is disabled (the endpoint presents a chain OpenSSL rejects) —
|
|
111
|
+
the same workaround as MOSPI's own reference client. UA and env proxies
|
|
112
|
+
are preset.
|
|
113
|
+
"""
|
|
114
|
+
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
115
|
+
ssl_context = ssl.create_default_context()
|
|
116
|
+
ssl_context.minimum_version = ssl.TLSVersion.TLSv1_2
|
|
117
|
+
ssl_context.options |= getattr(ssl, 'OP_LEGACY_SERVER_CONNECT', 0x4)
|
|
118
|
+
ssl_context.check_hostname = False
|
|
119
|
+
ssl_context.verify_mode = ssl.CERT_NONE
|
|
120
|
+
session = requests.Session()
|
|
121
|
+
session.headers.update({'User-Agent': USER_AGENT})
|
|
122
|
+
session.verify = False
|
|
123
|
+
session.mount('https://', _LegacyRenegotiationAdapter(ssl_context))
|
|
124
|
+
proxies = proxies_from_env()
|
|
125
|
+
if proxies:
|
|
126
|
+
session.proxies = proxies
|
|
127
|
+
return session
|