algierex 1.0.0__py3-none-any.whl

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.
algierex/__init__.py ADDED
@@ -0,0 +1,33 @@
1
+ """Python SDK for the Algierex market data API: the Algerian exchange's
2
+ prices, sessions, Dzair index, OAT yield curve, exchange rates, company
3
+ events and news.
4
+
5
+ from algierex import Algierex
6
+ client = Algierex() # reads ALGIEREX_API_KEY
7
+ prices = client.get_prices("SAI", from_="2026-01-01")
8
+ """
9
+
10
+ from ._client import Algierex
11
+ from ._core import (
12
+ AlgierexError,
13
+ AuthenticationError,
14
+ BadRequestError,
15
+ NotFoundError,
16
+ PaymentRequiredError,
17
+ RateLimitError,
18
+ to_frame,
19
+ )
20
+ from ._types import * # noqa: F401,F403 (response TypedDicts)
21
+
22
+ __version__ = "1.0.0"
23
+
24
+ __all__ = [
25
+ "Algierex",
26
+ "AlgierexError",
27
+ "AuthenticationError",
28
+ "BadRequestError",
29
+ "NotFoundError",
30
+ "PaymentRequiredError",
31
+ "RateLimitError",
32
+ "to_frame",
33
+ ]
algierex/_client.py ADDED
@@ -0,0 +1,167 @@
1
+ """The Algierex API client. Generated by sdk/generate.mjs from the OpenAPI spec. Do not edit; run `npm run sdk`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import urllib.parse
6
+ from typing import Iterator, List, Literal, Optional
7
+
8
+ from ._core import _Core
9
+ from ._types import CalendarEvent, FxLatest, FxRow, IndexDay, NewsItem, PriceDay, Session, Ticker, YieldCurve
10
+
11
+
12
+ def _quote(value: str) -> str:
13
+ return urllib.parse.quote(str(value), safe="")
14
+
15
+
16
+ class Algierex(_Core):
17
+ """Client for the Algierex market data API (https://algierex.com/api/v1).
18
+
19
+ client = Algierex() # reads ALGIEREX_API_KEY
20
+ prices = client.get_prices("SAI", from_="2026-01-01")
21
+ """
22
+
23
+ def list_tickers(self) -> List[Ticker]:
24
+ """List securities.
25
+
26
+ Every listed security with its names in English, French and Arabic,
27
+ sector, ISIN and capital facts.
28
+ """
29
+ return self._data("/tickers")
30
+
31
+ def get_prices(self, ticker: str, *, from_: Optional[str] = None, to: Optional[str] = None, page_size: Optional[int] = None) -> List[PriceDay]:
32
+ """Daily prices for one security.
33
+
34
+ One row per trading day, oldest first, across every market the security
35
+ has traded in. Several trade lines in one session (common for OATs) are
36
+ merged into one day: volumes add up and the last price is the close.
37
+
38
+ Args:
39
+ ticker: Symbol, for example SAI. Not case sensitive.
40
+ from_: First date to include (YYYY-MM-DD).
41
+ to: Last date to include (YYYY-MM-DD).
42
+ page_size: Rows per request, 1 to 1000 (default 1000). Paging is automatic.
43
+ """
44
+ return list(self.iter_prices(ticker, from_=from_, to=to, page_size=page_size))
45
+
46
+ def iter_prices(self, ticker: str, *, from_: Optional[str] = None, to: Optional[str] = None, page_size: Optional[int] = None) -> Iterator[PriceDay]:
47
+ """Like get_prices, but yields rows as pages arrive, for long histories."""
48
+ return self._pages(f"/prices/{_quote(ticker)}", {"from": from_, "to": to}, page_size)
49
+
50
+ def list_sessions(self, *, market: Optional[Literal["main", "bond", "sme", "oat"]] = None, from_: Optional[str] = None, to: Optional[str] = None, page_size: Optional[int] = None) -> List[Session]:
51
+ """Market sessions.
52
+
53
+ One market's sessions with their totals, oldest first. Premium and PI
54
+ securities trade in main market sessions.
55
+
56
+ Args:
57
+ market: main (equities), sme (growth market), bond (corporate bonds) or oat (Treasury bonds).
58
+ from_: First date to include (YYYY-MM-DD).
59
+ to: Last date to include (YYYY-MM-DD).
60
+ page_size: Rows per request, 1 to 1000 (default 1000). Paging is automatic.
61
+ """
62
+ return list(self.iter_sessions(market=market, from_=from_, to=to, page_size=page_size))
63
+
64
+ def iter_sessions(self, *, market: Optional[Literal["main", "bond", "sme", "oat"]] = None, from_: Optional[str] = None, to: Optional[str] = None, page_size: Optional[int] = None) -> Iterator[Session]:
65
+ """Like list_sessions, but yields rows as pages arrive, for long histories."""
66
+ return self._pages("/sessions", {"market": market, "from": from_, "to": to}, page_size)
67
+
68
+ def get_dzair_index(self, *, from_: Optional[str] = None, to: Optional[str] = None, page_size: Optional[int] = None) -> List[IndexDay]:
69
+ """Dzair index.
70
+
71
+ The Dzair index close for each session date, oldest first.
72
+
73
+ Args:
74
+ from_: First date to include (YYYY-MM-DD).
75
+ to: Last date to include (YYYY-MM-DD).
76
+ page_size: Rows per request, 1 to 1000 (default 1000). Paging is automatic.
77
+ """
78
+ return list(self.iter_dzair_index(from_=from_, to=to, page_size=page_size))
79
+
80
+ def iter_dzair_index(self, *, from_: Optional[str] = None, to: Optional[str] = None, page_size: Optional[int] = None) -> Iterator[IndexDay]:
81
+ """Like get_dzair_index, but yields rows as pages arrive, for long histories."""
82
+ return self._pages("/index/dzair", {"from": from_, "to": to}, page_size)
83
+
84
+ def get_yield_curve(self, *, date: Optional[str] = None) -> YieldCurve:
85
+ """OAT yield curve.
86
+
87
+ A Nelson-Siegel curve fitted to Algerian Treasury bonds (OATs) that
88
+ actually traded in the last 60 OAT sessions, with the bonds behind it.
89
+ Each bond counts at its most recent trade in that window, so a point can
90
+ be up to 60 sessions old. `fit` is null when fewer than five bonds
91
+ traded.
92
+
93
+ Args:
94
+ date: The curve as of the last OAT session on or before this date. Omit for the latest.
95
+ """
96
+ return self._data("/yield-curve", {"date": date})
97
+
98
+ def list_fx(self) -> List[FxLatest]:
99
+ """Latest exchange rates.
100
+
101
+ The latest rate per currency and source, in dinars per unit. `official`
102
+ is the Bank of Algeria rate; `exchangedz` is the parallel market.
103
+ """
104
+ return self._data("/fx")
105
+
106
+ def get_fx_history(self, code: str, *, source: Optional[Literal["official", "exchangedz"]] = None, from_: Optional[str] = None, to: Optional[str] = None, page_size: Optional[int] = None) -> List[FxRow]:
107
+ """Exchange rate history.
108
+
109
+ One currency's daily rates from one source, oldest first, in dinars per
110
+ unit. Official rates the upstream feed sent inverted are left out.
111
+
112
+ Args:
113
+ code: Currency code, for example USD or EUR.
114
+ source: official (Bank of Algeria) or exchangedz (parallel market).
115
+ from_: First date to include (YYYY-MM-DD).
116
+ to: Last date to include (YYYY-MM-DD).
117
+ page_size: Rows per request, 1 to 1000 (default 1000). Paging is automatic.
118
+ """
119
+ return list(self.iter_fx_history(code, source=source, from_=from_, to=to, page_size=page_size))
120
+
121
+ def iter_fx_history(self, code: str, *, source: Optional[Literal["official", "exchangedz"]] = None, from_: Optional[str] = None, to: Optional[str] = None, page_size: Optional[int] = None) -> Iterator[FxRow]:
122
+ """Like get_fx_history, but yields rows as pages arrive, for long histories."""
123
+ return self._pages(f"/fx/{_quote(code)}", {"source": source, "from": from_, "to": to}, page_size)
124
+
125
+ def get_calendar(self, *, from_: Optional[str] = None, to: Optional[str] = None, ticker: Optional[str] = None, locale: Optional[Literal["en", "fr", "ar"]] = None, page_size: Optional[int] = None) -> List[CalendarEvent]:
126
+ """Company events calendar.
127
+
128
+ General meetings, dividend and coupon payments, subscription periods and
129
+ listings, past and upcoming, ordered by date. The same calendar the
130
+ website shows: dates taken from news items, merged with a curated
131
+ calendar, without duplicates.
132
+
133
+ Args:
134
+ from_: First date to include (YYYY-MM-DD).
135
+ to: Last date to include (YYYY-MM-DD).
136
+ ticker: Only events for this security. Not case sensitive.
137
+ locale: Language of labels and titles: en, fr or ar. Text missing in that language falls back to the French original.
138
+ page_size: Rows per request, 1 to 1000 (default 1000). Paging is automatic.
139
+ """
140
+ return list(self.iter_calendar(from_=from_, to=to, ticker=ticker, locale=locale, page_size=page_size))
141
+
142
+ def iter_calendar(self, *, from_: Optional[str] = None, to: Optional[str] = None, ticker: Optional[str] = None, locale: Optional[Literal["en", "fr", "ar"]] = None, page_size: Optional[int] = None) -> Iterator[CalendarEvent]:
143
+ """Like get_calendar, but yields rows as pages arrive, for long histories."""
144
+ return self._pages("/calendar", {"from": from_, "to": to, "ticker": ticker, "locale": locale}, page_size)
145
+
146
+ def list_news(self, *, ticker: Optional[str] = None, source: Optional[Literal["cosob-visa", "cosob-decision", "cosob-communique", "sgbv-avis", "sgbv-press", "bank-of-algeria", "issuer-ir", "editorial"]] = None, from_: Optional[str] = None, to: Optional[str] = None, locale: Optional[Literal["en", "fr", "ar"]] = None, include_routine: Optional[bool] = None, page_size: Optional[int] = None) -> List[NewsItem]:
147
+ """News.
148
+
149
+ News from COSOB, the exchange, the Bank of Algeria and issuers, newest
150
+ first, with a summary in the asked language and a link to the full
151
+ article on Algierex. Routine trading-parameter notices are left out
152
+ unless `includeRoutine=true`.
153
+
154
+ Args:
155
+ ticker: Only news about this security. Not case sensitive.
156
+ source: Only news from one origin.
157
+ from_: First publication day to include (YYYY-MM-DD, Algiers time).
158
+ to: Last publication day to include (YYYY-MM-DD, Algiers time).
159
+ locale: Language of labels and titles: en, fr or ar. Text missing in that language falls back to the French original.
160
+ include_routine: Also return routine trading-parameter notices.
161
+ page_size: Rows per request, 1 to 1000 (default 1000). Paging is automatic.
162
+ """
163
+ return list(self.iter_news(ticker=ticker, source=source, from_=from_, to=to, locale=locale, include_routine=include_routine, page_size=page_size))
164
+
165
+ def iter_news(self, *, ticker: Optional[str] = None, source: Optional[Literal["cosob-visa", "cosob-decision", "cosob-communique", "sgbv-avis", "sgbv-press", "bank-of-algeria", "issuer-ir", "editorial"]] = None, from_: Optional[str] = None, to: Optional[str] = None, locale: Optional[Literal["en", "fr", "ar"]] = None, include_routine: Optional[bool] = None, page_size: Optional[int] = None) -> Iterator[NewsItem]:
166
+ """Like list_news, but yields rows as pages arrive, for long histories."""
167
+ return self._pages("/news", {"ticker": ticker, "source": source, "from": from_, "to": to, "locale": locale, "includeRoutine": include_routine}, page_size)
algierex/_core.py ADDED
@@ -0,0 +1,183 @@
1
+ """HTTP core of the Algierex SDK: requests, errors, retries, paging.
2
+
3
+ Hand-written. The API methods live in _client.py, generated from the
4
+ OpenAPI spec by sdk/generate.mjs.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import os
11
+ import time
12
+ import urllib.error
13
+ import urllib.parse
14
+ import urllib.request
15
+ from typing import Any, Callable, Dict, Iterator, List, Optional
16
+
17
+ DEFAULT_BASE_URL = "https://algierex.com/api/v1"
18
+ # The API's largest page: fewer calls against the plan's limits.
19
+ DEFAULT_PAGE_SIZE = 1000
20
+ USER_AGENT = "algierex-python"
21
+
22
+
23
+ class AlgierexError(Exception):
24
+ """An error answered by the API, or a failure to reach it."""
25
+
26
+ def __init__(self, message: str, status: Optional[int] = None, code: Optional[str] = None):
27
+ super().__init__(message)
28
+ self.status = status
29
+ self.code = code
30
+
31
+
32
+ class BadRequestError(AlgierexError):
33
+ """400: a parameter is not valid."""
34
+
35
+
36
+ class AuthenticationError(AlgierexError):
37
+ """401: the key is missing, wrong, or revoked."""
38
+
39
+
40
+ class PaymentRequiredError(AlgierexError):
41
+ """402: the key's account has no active plan, or its last payment failed."""
42
+
43
+
44
+ class NotFoundError(AlgierexError):
45
+ """404: no such security, currency, route, or date."""
46
+
47
+
48
+ class RateLimitError(AlgierexError):
49
+ """429: the plan's limit is used up. retry_after is in seconds."""
50
+
51
+ def __init__(self, message: str, retry_after: float, status: int = 429, code: str = "rate_limited"):
52
+ super().__init__(message, status, code)
53
+ self.retry_after = retry_after
54
+
55
+
56
+ _ERRORS = {400: BadRequestError, 401: AuthenticationError, 402: PaymentRequiredError, 404: NotFoundError}
57
+
58
+
59
+ class _Core:
60
+ def __init__(
61
+ self,
62
+ api_key: Optional[str] = None,
63
+ *,
64
+ base_url: str = DEFAULT_BASE_URL,
65
+ timeout: float = 30.0,
66
+ max_retries: int = 3,
67
+ max_retry_wait: float = 60.0,
68
+ _sleep: Callable[[float], None] = time.sleep,
69
+ ) -> None:
70
+ """Create a client.
71
+
72
+ api_key: your key (alx_...). Defaults to the ALGIEREX_API_KEY
73
+ environment variable.
74
+ max_retries: how many times to wait and retry when a rate limit is
75
+ hit (429). 0 raises RateLimitError at once.
76
+ max_retry_wait: the longest single wait, in seconds; a longer
77
+ Retry-After raises RateLimitError instead of sleeping.
78
+ """
79
+ key = api_key or os.environ.get("ALGIEREX_API_KEY")
80
+ if not key:
81
+ raise AuthenticationError("No API key. Pass api_key or set ALGIEREX_API_KEY.")
82
+ self._key = key
83
+ self._base_url = base_url.rstrip("/")
84
+ self._timeout = timeout
85
+ self._max_retries = max_retries
86
+ self._max_retry_wait = max_retry_wait
87
+ self._sleep = _sleep
88
+
89
+ def _request(self, path: str, params: Optional[Dict[str, Any]] = None) -> Any:
90
+ query = {k: _param(v) for k, v in (params or {}).items() if v is not None}
91
+ url = self._base_url + path + ("?" + urllib.parse.urlencode(query) if query else "")
92
+ attempt = 0
93
+ while True:
94
+ req = urllib.request.Request(
95
+ url,
96
+ headers={
97
+ "Authorization": f"Bearer {self._key}",
98
+ "Accept": "application/json",
99
+ "User-Agent": USER_AGENT,
100
+ },
101
+ )
102
+ try:
103
+ with urllib.request.urlopen(req, timeout=self._timeout) as res:
104
+ raw = res.read()
105
+ except urllib.error.HTTPError as err:
106
+ status = err.code
107
+ with err:
108
+ body = _json_or_none(err.read())
109
+ error = body.get("error") if isinstance(body, dict) else None
110
+ error = error if isinstance(error, dict) else {}
111
+ message = error.get("message") or f"HTTP {status}"
112
+ code = error.get("code")
113
+ if status == 429:
114
+ retry_after = _seconds(err.headers.get("Retry-After"))
115
+ if attempt < self._max_retries and retry_after <= self._max_retry_wait:
116
+ attempt += 1
117
+ self._sleep(retry_after)
118
+ continue
119
+ raise RateLimitError(message, retry_after) from None
120
+ raise _ERRORS.get(status, AlgierexError)(message, status, code) from None
121
+ except urllib.error.URLError as err:
122
+ raise AlgierexError(f"Could not reach {self._base_url}: {err.reason}") from None
123
+ except OSError as err: # timeouts and dropped connections
124
+ raise AlgierexError(f"Could not reach {self._base_url}: {err}") from None
125
+ body = _json_or_none(raw)
126
+ if body is None:
127
+ raise AlgierexError(f"The API answered with something that is not JSON ({url}).")
128
+ return body
129
+
130
+ def _data(self, path: str, params: Optional[Dict[str, Any]] = None) -> Any:
131
+ return self._request(path, params)["data"]
132
+
133
+ def _pages(self, path: str, params: Dict[str, Any], page_size: Optional[int]) -> Iterator[Any]:
134
+ """Every row of a paged route, following nextCursor to the end."""
135
+ params = dict(params, limit=page_size or DEFAULT_PAGE_SIZE)
136
+ seen = set()
137
+ while True:
138
+ body = self._request(path, params)
139
+ yield from body["data"]
140
+ cursor = body.get("nextCursor")
141
+ if not cursor:
142
+ return
143
+ if cursor in seen:
144
+ raise AlgierexError("The API returned the same page cursor twice; stopping to avoid a loop.")
145
+ seen.add(cursor)
146
+ params["cursor"] = cursor
147
+
148
+
149
+ def _param(value: Any) -> str:
150
+ if isinstance(value, bool):
151
+ return "true" if value else "false"
152
+ return str(value)
153
+
154
+
155
+ def _seconds(value: Optional[str]) -> float:
156
+ """Retry-After in seconds; 1 when missing or not a number."""
157
+ try:
158
+ return max(0.0, float(value)) if value else 1.0
159
+ except ValueError:
160
+ return 1.0
161
+
162
+
163
+ def _json_or_none(raw: bytes) -> Any:
164
+ try:
165
+ return json.loads(raw.decode("utf-8"))
166
+ except (ValueError, UnicodeDecodeError):
167
+ return None
168
+
169
+
170
+ def to_frame(rows: List[Dict[str, Any]], index: Optional[str] = "date"):
171
+ """Turn rows into a pandas DataFrame, indexed by date when there is one.
172
+
173
+ Needs pandas (pip install "algierex[pandas]").
174
+ """
175
+ try:
176
+ import pandas as pd # type: ignore[import-not-found]
177
+ except ImportError as err: # pragma: no cover
178
+ raise ImportError('to_frame needs pandas: pip install "algierex[pandas]"') from err
179
+ frame = pd.DataFrame(rows)
180
+ if index and index in frame.columns:
181
+ frame[index] = pd.to_datetime(frame[index])
182
+ frame = frame.set_index(index)
183
+ return frame
algierex/_types.py ADDED
@@ -0,0 +1,136 @@
1
+ """Response types. Generated by sdk/generate.mjs from the OpenAPI spec. Do not edit; run `npm run sdk`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Dict, List, Literal, Optional, TypedDict
6
+
7
+ class Ticker(TypedDict):
8
+ ticker: str # Symbol, for example SAI.
9
+ nameEn: str # Name in English.
10
+ nameFr: Optional[str] # Name in French.
11
+ nameAr: Optional[str] # Name in Arabic.
12
+ descEn: Optional[str] # Short description in English.
13
+ descFr: Optional[str] # Short description in French.
14
+ descAr: Optional[str] # Short description in Arabic.
15
+ isin: Optional[str] # ISIN code.
16
+ sector: Optional[str] # Sector, for example pharma or banking.
17
+ listingDate: Optional[str] # First listing day.
18
+ sharesOutstanding: Optional[float] # Shares issued.
19
+ parValueDZD: Optional[float] # Par value in DZD.
20
+ freeFloatPct: Optional[float] # Percent of shares free to trade.
21
+ stateOwnedPct: Optional[float] # Percent held by the state.
22
+
23
+
24
+ class PriceDay(TypedDict):
25
+ date: str # Session date.
26
+ sessionNumber: int # The exchange's session number.
27
+ market: Literal["main", "bond", "sme", "oat", "premium", "pi"] # Market the security traded in that day.
28
+ close: float # Closing price in DZD. For OATs, percent of face value.
29
+ open: Optional[float] # Opening price, when the bulletin gives one.
30
+ previousClose: Optional[float] # Previous close, when the bulletin gives one.
31
+ volume: float # Units traded.
32
+ value: float # Value traded in DZD.
33
+ transactions: int # Number of trades.
34
+
35
+
36
+ class Session(TypedDict):
37
+ date: str # Session date.
38
+ sessionNumber: int # The exchange's session number.
39
+ market: Literal["main", "bond", "sme", "oat"] # Market.
40
+ volume: float # Units traded in the market that session.
41
+ value: float # Value traded in DZD.
42
+ transactions: int # Number of trades.
43
+
44
+
45
+ class IndexDay(TypedDict):
46
+ date: str # Session date.
47
+ close: float # Index close.
48
+ change: float # Change from the previous close, in points.
49
+ changePct: float # Change from the previous close, in percent.
50
+ previousClose: float # Previous close.
51
+
52
+
53
+ class YieldCurvePointsItem(TypedDict):
54
+ ticker: str # OAT symbol.
55
+ maturityDate: str # Maturity date.
56
+ yearsToMaturity: float # Years from asOf to maturity.
57
+ yieldRate: float # Yield in percent at the bond's most recent trade.
58
+
59
+
60
+ YieldCurveFitCurveItem = TypedDict("YieldCurveFitCurveItem", {
61
+ "years": float, # Maturity in years.
62
+ "yield": float, # Fitted yield in percent.
63
+ })
64
+
65
+
66
+ # null when fewer than five bonds traded.
67
+ YieldCurveFit = TypedDict("YieldCurveFit", {
68
+ "beta0": float, # Long-term level.
69
+ "beta1": float, # Short-term component.
70
+ "beta2": float, # Curvature (medium-term hump).
71
+ "lambda": float, # Decay, in years: where the curvature peaks.
72
+ "rmse": float, # Root mean squared error of the fit, in percentage points.
73
+ "rSquared": float, # Share of yield variance across the bonds that the curve explains.
74
+ "shortRate": float, # Model yield at maturity zero: beta0 + beta1.
75
+ "longRate": float, # Model yield at very long maturity: beta0.
76
+ "curve": List[YieldCurveFitCurveItem], # The fitted curve sampled across the observed maturities.
77
+ })
78
+
79
+
80
+ class YieldCurve(TypedDict):
81
+ asOf: str # The OAT session the curve is dated to.
82
+ staleCount: int # OATs listed in that session's bulletin with no trade in the last 60 OAT sessions. They are left out of the fit.
83
+ points: List[YieldCurvePointsItem] # The bonds behind the fit, shortest maturity first.
84
+ fit: Optional[YieldCurveFit] # null when fewer than five bonds traded.
85
+
86
+
87
+ class FxLatest(TypedDict):
88
+ assetCode: str # Currency code, for example USD.
89
+ assetType: Literal["CURRENCY", "CRYPTO", "METAL"] # Kind of asset.
90
+ source: Literal["official", "exchangedz"] # official: Bank of Algeria. exchangedz: parallel market.
91
+ date: str # Date of the rate.
92
+ buyRate: float # Buy rate in DZD per unit.
93
+ sellRate: float # Sell rate in DZD per unit.
94
+
95
+
96
+ class CalendarEventNews(TypedDict):
97
+ """The news item the date comes from; null for curated events."""
98
+
99
+ source: str # News origin.
100
+ sourceId: str # News id at the origin.
101
+ title: str # Title of the news item, in the asked language.
102
+ url: str # The article on Algierex.
103
+
104
+
105
+ class CalendarEvent(TypedDict):
106
+ id: str # Stable id of the event.
107
+ date: str # Day of the event.
108
+ kind: str # ago: ordinary general meeting. agex: extraordinary general meeting. dividend_payment and coupon_payment: payment day. subscription_open and subscription_close: an offer's subscription period. listing_start: first trading day. session_cancelled: no session that day. result: results publication. deadline: a filing or application deadline. other: anything else.
109
+ label: str # What happens, in the asked language.
110
+ ticker: Optional[str] # The security, when the event is about one.
111
+ market: Optional[str] # The security's market, for curated events.
112
+ news: Optional[CalendarEventNews] # The news item the date comes from; null for curated events.
113
+
114
+
115
+ class NewsItem(TypedDict):
116
+ source: str # cosob-visa: COSOB approval of an offer document. cosob-decision: COSOB decision. cosob-communique: COSOB press release. sgbv-avis: the exchange's official notice. sgbv-press: the exchange's press and information notes. bank-of-algeria: Bank of Algeria publication. issuer-ir: a company's own investor news. editorial: written by Algierex.
117
+ sourceId: str # Id at the origin.
118
+ title: str # Title in the asked language.
119
+ summary: Optional[str] # Short summary in the asked language.
120
+ ticker: Optional[str] # The security the item is about.
121
+ market: Optional[str] # That security's market.
122
+ tags: List[str] # Topics, for example listing or dividend.
123
+ originalLanguage: Literal["fr", "en", "ar"] # Language the origin published in.
124
+ publishedAt: str # Publication time (UTC).
125
+ dateIsApprox: bool # True when the origin gave no exact date.
126
+ sourceUrl: str # Page at the origin.
127
+ pdfUrl: Optional[str] # The original document, when there is one.
128
+ url: str # The article on Algierex, in the asked language.
129
+
130
+
131
+ class FxRow(TypedDict):
132
+ date: str # Date of the rate.
133
+ assetCode: str # Currency code.
134
+ source: Literal["official", "exchangedz"] # official: Bank of Algeria. exchangedz: parallel market.
135
+ buyRate: float # Buy rate in DZD per unit.
136
+ sellRate: float # Sell rate in DZD per unit.
algierex/py.typed ADDED
File without changes
@@ -0,0 +1,109 @@
1
+ Metadata-Version: 2.4
2
+ Name: algierex
3
+ Version: 1.0.0
4
+ Summary: Python SDK for the Algierex market data API (the Algerian exchange)
5
+ Author-email: Algierex <contact@algierex.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://algierex.com/developers
8
+ Project-URL: Documentation, https://algierex.com/developers/docs
9
+ Keywords: algeria,algiers,stock exchange,market data,finance,api
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Office/Business :: Financial :: Investment
13
+ Classifier: Typing :: Typed
14
+ Requires-Python: >=3.9
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Provides-Extra: pandas
18
+ Requires-Dist: pandas>=1.5; extra == "pandas"
19
+ Dynamic: license-file
20
+
21
+ # algierex (Python)
22
+
23
+ Python SDK for the [Algierex market data API](https://algierex.com/developers/docs): the Algerian exchange's daily prices back to 1999, sessions, the Dzair index, the OAT yield curve, exchange rates, company events and news.
24
+
25
+ No dependencies. Python 3.9 or newer.
26
+
27
+ ## Install
28
+
29
+ ```sh
30
+ pip install algierex # or "algierex[pandas]" for to_frame()
31
+ ```
32
+
33
+ ## Get a key
34
+
35
+ Create a key and choose a plan at [algierex.com/developers](https://algierex.com/developers). Then:
36
+
37
+ ```sh
38
+ export ALGIEREX_API_KEY=alx_...
39
+ ```
40
+
41
+ ## Example
42
+
43
+ ```python
44
+ from algierex import Algierex, PaymentRequiredError, to_frame
45
+
46
+ client = Algierex() # reads ALGIEREX_API_KEY
47
+
48
+ try:
49
+ client.list_tickers()
50
+ except PaymentRequiredError as err:
51
+ raise SystemExit(f"{err}") # no active plan: choose one at algierex.com/developers
52
+
53
+ # Every daily close of Saidal since 2025, as a pandas DataFrame.
54
+ prices = to_frame(client.get_prices("SAI", from_="2025-01-01"))
55
+ print(prices["close"].tail())
56
+
57
+ # The OAT yield curve on a past date.
58
+ curve = client.get_yield_curve(date="2026-09-30")
59
+ print(curve["asOf"], curve["fit"]["rSquared"] if curve["fit"] else "not enough trades")
60
+
61
+ # Upcoming company events, in French.
62
+ for event in client.get_calendar(from_="2026-11-01", locale="fr"):
63
+ print(event["date"], event["ticker"], event["label"])
64
+ ```
65
+
66
+ ## Methods
67
+
68
+ | Method | Route |
69
+ |---|---|
70
+ | `list_tickers()` | `GET /tickers` |
71
+ | `get_prices(ticker, from_=, to=)` | `GET /prices/{ticker}` |
72
+ | `list_sessions(market=, from_=, to=)` | `GET /sessions` |
73
+ | `get_dzair_index(from_=, to=)` | `GET /index/dzair` |
74
+ | `get_yield_curve(date=)` | `GET /yield-curve` |
75
+ | `list_fx()` | `GET /fx` |
76
+ | `get_fx_history(code, source=, from_=, to=)` | `GET /fx/{code}` |
77
+ | `get_calendar(from_=, to=, ticker=, locale=)` | `GET /calendar` |
78
+ | `list_news(ticker=, source=, from_=, to=, locale=, include_routine=)` | `GET /news` |
79
+
80
+ `from` is a Python keyword, so the parameter is `from_`. Every response is plain JSON (dicts and lists), typed with `TypedDict`s.
81
+
82
+ ## Paging
83
+
84
+ Paging is automatic: list methods return every row. For long histories, the `iter_*` twin yields rows as pages arrive:
85
+
86
+ ```python
87
+ for day in client.iter_prices("SAI"):
88
+ ...
89
+ ```
90
+
91
+ `page_size` (1 to 1000, default 1000) sets rows per request; the default keeps calls against your plan's limits low.
92
+
93
+ ## Errors
94
+
95
+ Every error raises a subclass of `AlgierexError`, with `.status` and `.code`:
96
+
97
+ | Exception | When |
98
+ |---|---|
99
+ | `BadRequestError` | 400: a parameter is not valid |
100
+ | `AuthenticationError` | 401: the key is missing, wrong, or revoked |
101
+ | `PaymentRequiredError` | 402: no active plan, or the last payment failed |
102
+ | `NotFoundError` | 404: no such security, currency, or date |
103
+ | `RateLimitError` | 429: the plan's limit is used up; `.retry_after` in seconds |
104
+
105
+ On a 429 the client waits `Retry-After` and retries, up to `max_retries` (default 3) and only for waits up to `max_retry_wait` seconds (default 60). `Algierex(max_retries=0)` raises at once. Server errors (5xx), timeouts (`timeout=30` seconds) and network failures raise `AlgierexError` and are not retried.
106
+
107
+ ## Regenerating
108
+
109
+ This SDK is generated from the API's OpenAPI spec. From the repository root, after `npm install`: `npm run sdk`.
@@ -0,0 +1,10 @@
1
+ algierex/__init__.py,sha256=MtUNGfpWPkrSg7d4Lk4MGt0einnUz-gbhDlQNVV912w,782
2
+ algierex/_client.py,sha256=PBfS7mIFlxB-oGa6FwKlsaJ2l9Q60J8rZlQrUpG0PnI,9836
3
+ algierex/_core.py,sha256=Ghe8jHM_0uOHZ05aT5GM2kiKquZRMARPaGzhdqDhUI0,6777
4
+ algierex/_types.py,sha256=jKP9FJEWo_o7cEwpFUH3feziYoSLMncSJjg5ivJP9qY,6527
5
+ algierex/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ algierex-1.0.0.dist-info/licenses/LICENSE,sha256=jCLkdpnzpfFZw7MrI8Q7zgJBNJ7m0gQbvjRZzO0dxt4,1065
7
+ algierex-1.0.0.dist-info/METADATA,sha256=SgCjtsDwmuW9nMcblPN6McWZI-H0MsuyE-pf7HwxBcc,3983
8
+ algierex-1.0.0.dist-info/WHEEL,sha256=a-udL2m4V_6Yn8b-uKzWb12RjRxwwMo6ohYFM24tpaY,91
9
+ algierex-1.0.0.dist-info/top_level.txt,sha256=ghws0k9rtRiVzmP7fSKLwhiz7omu1uGt_lX8NlDSOXM,9
10
+ algierex-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Algierex
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ algierex