nepsepy 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.
nepsepy/__init__.py ADDED
@@ -0,0 +1,18 @@
1
+ """Public NEPSE market-data client (normal public-site flow only)."""
2
+
3
+ from .auth import ProveResponse, TokenState
4
+ from .checksum import base as checksum_base
5
+ from .checksum import current_day as checksum_day
6
+ from .checksum import variant_a as checksum_a
7
+ from .checksum import variant_b as checksum_b
8
+ from .checksum import variant_c as checksum_c
9
+ from .client import NepseClient
10
+ from .exceptions import (AuthExpiredError, BootstrapError, NepseError,
11
+ PublicEndpointError, RateLimitedError, WasmError)
12
+ from .wasm import WasmCleaner, redact, strip_at_indexes
13
+
14
+ __all__ = ["NepseClient", "ProveResponse", "TokenState", "WasmCleaner",
15
+ "redact", "strip_at_indexes", "NepseError", "BootstrapError",
16
+ "WasmError", "AuthExpiredError", "RateLimitedError",
17
+ "PublicEndpointError", "checksum_base", "checksum_day",
18
+ "checksum_a", "checksum_b", "checksum_c"]
nepsepy/_company.py ADDED
@@ -0,0 +1,115 @@
1
+ """Per-security data and company tabs. endpoints (mixin for NepseClient)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Optional
6
+
7
+
8
+ class CompanyMixin:
9
+ # -- per-security data (GET; mirrors company tabs) --------------------------
10
+ def security_detail(self, security_id: int) -> dict:
11
+ """{securityData, securityMcsData} (Market Depth page header)."""
12
+ return self.get_json(f"/api/nots/security/{security_id}") # type: ignore[return-value]
13
+
14
+ def security_profile(self, security_id: int) -> dict:
15
+ return self.get_json(f"/api/nots/security/profile/{security_id}") # type: ignore[return-value]
16
+
17
+ def board_of_directors(self, security_id: int) -> list:
18
+ return self.get_json(f"/api/nots/security/boardOfDirectors/{security_id}") # type: ignore[return-value]
19
+
20
+ def corporate_actions(self, security_id: int) -> list:
21
+ """Bonus/cash/right [{fiscalYear, bonusPercentage, ...}]."""
22
+ return self.get_json(f"/api/nots/security/corporate-actions/{security_id}") # type: ignore[return-value]
23
+
24
+ def financial_reports(self, security_id: int) -> list:
25
+ return self.get_json(f"/api/nots/application/reports/{security_id}") # type: ignore[return-value]
26
+
27
+ def agm(self, security_id: int) -> list:
28
+ return self.get_json(f"/api/nots/application/agm/{security_id}") # type: ignore[return-value]
29
+
30
+ def security_company_news(self, security_id: int) -> list:
31
+ return self.get_json(f"/api/nots/application/company-news/{security_id}") # type: ignore[return-value]
32
+
33
+ def market_security(self, security_id: int) -> dict:
34
+ """Flat daily-trade snapshot + nested security (Listing Information)."""
35
+ return self.get_json(f"/api/nots/market/security/{security_id}") # type: ignore[return-value]
36
+
37
+ def security_market_picture(self, security_id: int) -> dict:
38
+ """Compact quote {receivedDateTime, securityId, lastTradedPrice,
39
+ openPrice, highPrice, lowPrice} (site's getSecurityDetailsFromMarketPicture)."""
40
+ return self.get_json(f"/api/nots/security-detail/{security_id}") # type: ignore[return-value]
41
+
42
+ def dividends(self, security_id: int) -> list:
43
+ """Dividend applications/news for a security (site's getSecurityDividendNewsById)."""
44
+ return self.get_json(f"/api/nots/application/dividend/{security_id}") # type: ignore[return-value]
45
+
46
+ def book_close(self, news_id: int) -> dict:
47
+ """Book-closure news by id (site's getSecurityBookCloseNewsById).
48
+
49
+ NOTE: currently HTTP 404 for every id tried (security ids and
50
+ company-news ids alike), so this is mapped-but-dead like
51
+ security_floorsheet() until the backend serves it again.
52
+ """
53
+ return self.get_json(f"/api/nots/news/book-close/{news_id}") # type: ignore[return-value]
54
+
55
+ def security_price_history(self, security_id: int, page: int = 1,
56
+ size: int = 20,
57
+ business_date: Optional[str] = None) -> dict:
58
+ """Price History tab (Spring page of OHLCV rows; page 1-based)."""
59
+ query = f"?&size={size}"
60
+ if business_date:
61
+ query += f"&businessDate={business_date}"
62
+ if page > 1:
63
+ query = f"?page={page - 1}&size={size}" + (
64
+ f"&businessDate={business_date}" if business_date else "")
65
+ return self.get_json(f"/api/nots/market/security/price/{security_id}{query}") # type: ignore[return-value]
66
+
67
+ def stock_trading_history(self, security_id: int, page: int = 1,
68
+ size: int = 20,
69
+ start: Optional[str] = None,
70
+ end: Optional[str] = None) -> dict:
71
+ """Stock Trading page (page 1-based; dates 'yyyy-MM-dd')."""
72
+ query = f"?&size={size}"
73
+ if start:
74
+ query += f"&startDate={start}"
75
+ if end:
76
+ query += f"&endDate={end}"
77
+ if page > 1:
78
+ query = f"?page={page - 1}&size={size}" + (
79
+ f"&startDate={start}" if start else "") + (
80
+ f"&endDate={end}" if end else "")
81
+ return self.get_json(f"/api/nots/market/history/security/{security_id}{query}") # type: ignore[return-value]
82
+
83
+ def trading_average(self, n_days: int = 120,
84
+ business_date: Optional[str] = None,
85
+ stock_id: Optional[int] = None) -> list:
86
+ """N-day average price per security (Trading Average page)."""
87
+ path = f"/api/nots/nepse-data/trading-average?nDays={n_days}"
88
+ if business_date:
89
+ path += f"&businessDate={business_date}"
90
+ if stock_id is not None:
91
+ path += f"&stockId={stock_id}"
92
+ return self.get_json(path) # type: ignore[return-value]
93
+
94
+ def market_cap_history(self, page: int = 1) -> list:
95
+ """Daily market caps [{businessDate, marCap, ...}] (filter client-side)."""
96
+ path = "/api/nots/nepse-data/marcapbydate/?"
97
+ if page > 1:
98
+ path = f"/api/nots/nepse-data/marcapbydate/?page={page - 1}"
99
+ return self.get_json(path) # type: ignore[return-value]
100
+
101
+ # -- signed POST: company data --------------------------------------------------
102
+ def company_info(self, security_id: int) -> dict:
103
+ """Company header {securityDailyTradeDto, security, ...} (detail page)."""
104
+ return self.post_json(f"/api/nots/security/{security_id}", # type: ignore[return-value]
105
+ {"id": self._signed("raw")})
106
+
107
+ def company_graph(self, security_id: int) -> list:
108
+ """Company OHLC history [{businessDate, openPrice, ...}] (chart tab)."""
109
+ return self.post_json(f"/api/nots/market/graphdata/{security_id}", # type: ignore[return-value]
110
+ {"id": self._signed("raw")})
111
+
112
+ def company_graph_intraday(self, security_id: int) -> list:
113
+ """Company intraday LTP (Charts Company tab, 1D range)."""
114
+ return self.post_json(f"/api/nots/market/graphdata/daily/{security_id}", # type: ignore[return-value]
115
+ {"id": self._signed("raw")})
nepsepy/_market.py ADDED
@@ -0,0 +1,101 @@
1
+ """Market data, indices, top tens and depth. endpoints (mixin for NepseClient)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Optional
6
+
7
+
8
+ class MarketMixin:
9
+ # -- market status / summary / ticker --------------------------------
10
+ def market_status(self) -> dict:
11
+ """Market open state (homepage + every page reads this)."""
12
+ return self.get_json("/api/nots/nepse-data/market-open") # type: ignore[return-value]
13
+
14
+ def market_summary(self) -> list:
15
+ """Homepage summary tiles [{detail, value}]."""
16
+ return self.get_json("/api/nots/market-summary/") # type: ignore[return-value]
17
+
18
+ def market_summary_history(self) -> list:
19
+ """Daily history [{businessDate, totalTurnover, ...}]."""
20
+ return self.get_json("/api/nots/market-summary-history") # type: ignore[return-value]
21
+
22
+ def ticker(self, index_id: int = 58) -> list:
23
+ """Live ticker strip [{symbol, ltp, ...}] (every page load)."""
24
+ return self.get_json(f"/api/nots/securityDailyTradeStat/{index_id}") # type: ignore[return-value]
25
+
26
+ def live_market(self) -> list:
27
+ """Full live snapshot (356 rows: OHLC, volumes, per-security indexId).
28
+
29
+ The Live Market *page* redirects to `/` while the market is closed,
30
+ but the API itself answers 200 regardless — verified live.
31
+ """
32
+ return self.get_json("/api/nots/lives-market") # type: ignore[return-value]
33
+
34
+ def nepse_indices(self) -> list:
35
+ """Index snapshot [{index, close, change, perChange, ...}]."""
36
+ return self.get_json("/api/nots/nepse-index") # type: ignore[return-value]
37
+
38
+ def sub_indices(self) -> list:
39
+ """Sub-index snapshot [{id, index, change, currentValue}]."""
40
+ return self.get_json("/api/nots") # type: ignore[return-value]
41
+
42
+ def indices_list(self) -> list:
43
+ """Index directory [{id, indexCode, indexName, ...}]."""
44
+ return self.get_json("/api/nots/index") # type: ignore[return-value]
45
+
46
+ def index_history(self, index_id: int = 58, page: int = 1,
47
+ size: int = 20) -> dict:
48
+ """Datewise index OHLC (Indices page; page is 1-based like the UI)."""
49
+ path = f"/api/nots/index/history/{index_id}?&size={size}"
50
+ if page > 1:
51
+ path = f"/api/nots/index/history/{index_id}?page={page - 1}&size={size}"
52
+ return self.get_json(path) # type: ignore[return-value]
53
+
54
+ def sector_summary(self, business_date: Optional[str] = None) -> list:
55
+ """Sector turnover (Sector Summary page; date 'yyyy-MM-dd')."""
56
+ path = "/api/nots/sectorwise"
57
+ if business_date:
58
+ path += f"?businessDate={business_date}"
59
+ return self.get_json(path) # type: ignore[return-value]
60
+
61
+ # -- top tens ------------------------------------------------------------
62
+ def _top_ten(self, kind: str, full: bool = False) -> list:
63
+ return self.get_json(f"/api/nots/top-ten/{kind}?all={str(full).lower()}") # type: ignore[return-value]
64
+
65
+ def top_gainers(self, full: bool = False) -> list:
66
+ """Top gainers [{symbol, ltp, pointChange, percentageChange, ...}]."""
67
+ return self._top_ten("top-gainer", full)
68
+
69
+ def top_losers(self, full: bool = False) -> list:
70
+ """Top losers (same shape as gainers)."""
71
+ return self._top_ten("top-loser", full)
72
+
73
+ def top_turnover(self, full: bool = False) -> list:
74
+ """Top by turnover."""
75
+ return self._top_ten("turnover", full)
76
+
77
+ def top_traded_shares(self, full: bool = False) -> list:
78
+ """Top by shares traded."""
79
+ return self._top_ten("trade", full)
80
+
81
+ def top_transactions(self, full: bool = False) -> list:
82
+ """Top by transaction count."""
83
+ return self._top_ten("transaction", full)
84
+
85
+ def top_active(self, full: bool = False) -> list:
86
+ """Most active scrips (trade quantity)."""
87
+ return self._top_ten("trade-qty", full)
88
+
89
+ # -- supply / demand / depth ----------------------------------------------
90
+ def supply_demand(self, full: bool = False) -> dict:
91
+ """Top supply/demand {supplyList, demandList}."""
92
+ return self.get_json( # type: ignore[return-value]
93
+ f"/api/nots/nepse-data/supplydemand?all={str(full).lower()}")
94
+
95
+ def market_depth(self, security_id: int) -> dict:
96
+ """Order book {totalBuyQty, totalSellQty, marketDepth{...}}."""
97
+ return self.get_json(f"/api/nots/nepse-data/marketdepth/{security_id}") # type: ignore[return-value]
98
+
99
+ def odd_lot_depth(self, security_id: int) -> dict:
100
+ """Odd-lot order book (same shape as market_depth)."""
101
+ return self.get_json(f"/api/nots/nepse-data/marketdepth-ol/{security_id}") # type: ignore[return-value]
nepsepy/_news.py ADDED
@@ -0,0 +1,167 @@
1
+ """News, calendar, captcha, files and reference data. endpoints (mixin for NepseClient)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Optional
6
+
7
+
8
+ class NewsMixin:
9
+ # -- news / notices / calendar -----------------------------------------------------
10
+ def notices(self, page: int = 1) -> dict:
11
+ """Notices (Spring page; page 1-based like the UI)."""
12
+ return self.get_json(f"/api/nots/news/notice/all?page={page - 1}") # type: ignore[return-value]
13
+
14
+ def disclosures(self) -> dict:
15
+ """Homepage disclosures {exchangeMessages, companyNews}."""
16
+ return self.get_json("/api/nots/news/companies/disclosure") # type: ignore[return-value]
17
+
18
+ def company_news_list(self) -> list:
19
+ """Corporate disclosures feed (8836 rows at capture time)."""
20
+ return self.get_json("/api/nots/news/media/company-news") # type: ignore[return-value]
21
+
22
+ def news_alerts(self, page: int = 1) -> list:
23
+ if page <= 1:
24
+ return self.get_json("/api/nots/news/media/news-and-alerts") # type: ignore[return-value]
25
+ return self.get_json(f"/api/nots/news/media/news-and-alerts/?page={page - 1}") # type: ignore[return-value]
26
+
27
+ def press_releases(self, page: int = 1) -> list:
28
+ if page <= 1:
29
+ return self.get_json("/api/nots/news/press-release") # type: ignore[return-value]
30
+ return self.get_json(f"/api/nots/news/press-release?page={page - 1}") # type: ignore[return-value]
31
+
32
+ def investor_awareness(self) -> list:
33
+ return self.get_json("/api/nots/news/media/investor-awareness") # type: ignore[return-value]
34
+
35
+ def holiday_years(self) -> list:
36
+ return self.get_json("/api/nots/holiday/year") # type: ignore[return-value]
37
+
38
+ def holidays(self, year: int) -> list:
39
+ """Market holidays [{holidayDate, holidayDescription}]."""
40
+ return self.get_json(f"/api/nots/holiday/list?year={year}") # type: ignore[return-value]
41
+
42
+ def menu(self) -> list:
43
+ return self.get_json("/api/web/menu/") # type: ignore[return-value]
44
+
45
+ def listing_info(self) -> dict:
46
+ """Listing Information page content {headline, body} (web CMS)."""
47
+ return self.get_json("/api/web/listing-info") # type: ignore[return-value]
48
+
49
+ def info_officer(self) -> dict:
50
+ """Information officer contact (web CMS)."""
51
+ return self.get_json("/api/web/info-officer") # type: ignore[return-value]
52
+
53
+ def about_introduction(self, page: Optional[int] = None) -> dict:
54
+ """About-Us introductions (Spring page; site passes ?page=0-based
55
+ only past the first page)."""
56
+ path = "/api/web/about-us/introduction"
57
+ if page is not None and page > 1:
58
+ path += f"?page={page - 1}"
59
+ return self.get_json(path) # type: ignore[return-value]
60
+
61
+ def about_structure(self, page: Optional[int] = None) -> dict:
62
+ """About-Us org structure (same paging as introduction)."""
63
+ path = "/api/web/about-us/structure"
64
+ if page is not None and page > 1:
65
+ path += f"?page={page - 1}"
66
+ return self.get_json(path) # type: ignore[return-value]
67
+
68
+ def contact_info(self) -> dict:
69
+ """Contact-Us info {contact, id} (web CMS)."""
70
+ return self.get_json("/api/web/about-us/contact-info") # type: ignore[return-value]
71
+
72
+ # -- captcha (suggestion-box flow; read-only steps only) -------------------
73
+ def captcha_challenge(self) -> dict:
74
+ """Captcha challenge {id} (public; image/reload hang off the id)."""
75
+ return self.get_json("/api/web/captcha/id") # type: ignore[return-value]
76
+
77
+ def captcha_image(self, challenge_id: str) -> bytes:
78
+ """Captcha image bytes (octet-stream). NOTE: the reload sibling
79
+ (web/captcha/reload/{id}) currently 500s server-side."""
80
+ return self.get(
81
+ f"/api/web/captcha/image/{challenge_id}").content
82
+
83
+ # -- file downloads (binary; verified live) --------------------------------
84
+ def fetch_application_file(self, encrypted_id: str) -> bytes:
85
+ """Application/AGM report file (PDF bytes; id from e.g. disclosures)."""
86
+ return self.get( # type: ignore[return-value]
87
+ f"/api/nots/application/fetchFiles?encryptedId={encrypted_id}").content
88
+
89
+ def fetch_notice_file(self, file_name: str) -> bytes:
90
+ """Notice attachment bytes (fileName from notices())."""
91
+ return self.get( # type: ignore[return-value]
92
+ f"/api/nots/news/notice/fetchFiles/{file_name}").content
93
+
94
+ def export_stock_csv(self, security_id: int,
95
+ start: Optional[str] = None,
96
+ end: Optional[str] = None) -> bytes:
97
+ """Stock Trading CSV export (site's export path; dates 'yyyy-MM-dd')."""
98
+ path = f"/api/nots/market/export/{security_id}"
99
+ query = "&".join(
100
+ [f"startDate={start}" for _ in [0] if start] +
101
+ [f"endDate={end}" for _ in [0] if end])
102
+ if query:
103
+ path += "?" + query
104
+ return self.get(path).content
105
+
106
+ def security_image(self, file_location: str) -> bytes:
107
+ """Security image envelope (logos, board portraits) — raw body.
108
+
109
+ The response is JSON ``{"content": "<base64>"}``, NOT raw image
110
+ bytes. To process: parse the JSON, take ``["content"]``, strip
111
+ whitespace, pad to a multiple of 4 (``+= "=" * (-len(s) % 4)``),
112
+ then base64-decode — you get JPEG/PNG bytes (magic ``ffd8ff``).
113
+ For web display the site's own bundle prefixes the string as
114
+ ``"data:image/PNG;base64," + content`` straight into an <img>.
115
+ ``fileLocation`` from e.g. ``security_profile()["logoFilePath"]``
116
+ (verified: BARUN logo decodes to a 25 KB JPEG).
117
+ NOTE: returns the raw envelope; decoding is left to the caller.
118
+ """
119
+ return self.get( # type: ignore[return-value]
120
+ f"/api/nots/security/getImage?fileLocation={file_location}").content
121
+
122
+ def security_file(self, file_location: str) -> bytes:
123
+ """Security file bytes (same fileLocation scheme) — raw bytes.
124
+
125
+ Unlike security_image(), this one returns the file directly
126
+ (verified: BARUN logo path yields ``image/jpeg`` + JFIF magic,
127
+ no envelope). Save the body as-is.
128
+ """
129
+ return self.get( # type: ignore[return-value]
130
+ f"/api/nots/security/fetchFiles?fileLocation={file_location}").content
131
+
132
+ # -- brokers / dealers (reference data) --------------------------------------
133
+ def brokers(self, page: int = 1, size: int = 20,
134
+ criteria: Optional[dict] = None) -> dict:
135
+ """Broker directory (Spring page; page 1-based like the UI).
136
+
137
+ criteria mirrors the Brokers search form: memberName,
138
+ contactPerson, contactNumber, memberCode, provinceId, districtId,
139
+ municipalityId. The site always sends the full object with
140
+ defaults, so missing keys are filled the same way.
141
+ """
142
+ if page > 1:
143
+ path = f"/api/nots/member?page={page - 1}&size={size}"
144
+ else:
145
+ path = f"/api/nots/member?&size={size}"
146
+ if criteria:
147
+ body = {"memberName": "", "contactPerson": "",
148
+ "contactNumber": "", "memberCode": "", "provinceId": 0,
149
+ "districtId": 0, "municipalityId": 0}
150
+ body.update(criteria)
151
+ return self.post_json(path, body) # type: ignore[return-value]
152
+ return self.get_json(path) # type: ignore[return-value]
153
+
154
+ def dealers(self, page: int = 1, size: int = 20,
155
+ criteria: Optional[dict] = None) -> dict:
156
+ """Dealer directory (same shape/conventions as brokers)."""
157
+ if page > 1:
158
+ path = f"/api/nots/member/dealer?page={page - 1}&size={size}"
159
+ else:
160
+ path = f"/api/nots/member/dealer?&size={size}"
161
+ if criteria:
162
+ body = {"memberName": "", "contactPerson": "",
163
+ "contactNumber": "", "memberCode": "", "provinceId": 0,
164
+ "districtId": 0, "municipalityId": 0}
165
+ body.update(criteria)
166
+ return self.post_json(path, body) # type: ignore[return-value]
167
+ return self.get_json(path) # type: ignore[return-value]
nepsepy/_prices.py ADDED
@@ -0,0 +1,101 @@
1
+ """Signed price, floorsheet and graph POSTs. endpoints (mixin for NepseClient)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Optional
6
+
7
+
8
+ class PricesMixin:
9
+ # -- signed POST: today price ----------------------------------------------
10
+ def today_price(self, page: int = 1, size: int = 20,
11
+ security_id: Optional[int] = None,
12
+ business_date: Optional[str] = None,
13
+ sort_by: str = "", sort_order: str = "") -> dict:
14
+ """Today's Price table (Spring page; page 1-based like the UI)."""
15
+ query = f"?&size={size}"
16
+ if security_id is not None:
17
+ query += f"&securityId={security_id}"
18
+ if business_date:
19
+ query += f"&businessDate={business_date}"
20
+ if sort_by:
21
+ query += f"&sort={sort_by},{sort_order}"
22
+ if page > 1:
23
+ query = query.replace("?", f"?page={page - 1}", 1)
24
+ return self.post_json( # type: ignore[return-value]
25
+ f"/api/nots/nepse-data/today-price{query}",
26
+ {"id": self._signed("b")})
27
+
28
+ def today_price_all(self, business_date: Optional[str] = None) -> list:
29
+ """Full Today's Price list behind the CSV export."""
30
+ path = "/api/nots/nepse-data/todays-price/"
31
+ if business_date:
32
+ path += f"?businessDate={business_date}"
33
+ return self.post_json(path, {"id": self._signed("b")}) # type: ignore[return-value]
34
+
35
+ # -- signed POST: floorsheet -------------------------------------------------
36
+ def floorsheets(self, page: int = 1, size: int = 20,
37
+ contract_no: Optional[int] = None,
38
+ stock_id: Optional[int] = None,
39
+ buyer_broker: Optional[int] = None,
40
+ seller_broker: Optional[int] = None,
41
+ sort_by: str = "contractId",
42
+ sort_order: str = "desc") -> dict:
43
+ """Floor Sheet page ({totalAmount, totalQty, totalTrades, floorsheets};
44
+ page 1-based like the UI)."""
45
+ query = f"?&size={size}"
46
+ if stock_id is not None:
47
+ query += f"&stockId={stock_id}"
48
+ if contract_no is not None:
49
+ query += f"&contractNo={contract_no}"
50
+ if buyer_broker is not None:
51
+ query += f"&buyerBroker={buyer_broker}"
52
+ if seller_broker is not None:
53
+ query += f"&sellerBroker={seller_broker}"
54
+ query += f"&sort={sort_by},{sort_order}"
55
+ if page > 1:
56
+ query = query.replace("?", f"?page={page - 1}", 1)
57
+ return self.post_json( # type: ignore[return-value]
58
+ f"/api/nots/nepse-data/floorsheet{query}",
59
+ {"id": self._signed("c")})
60
+
61
+ def security_floorsheet(self, security_id: int,
62
+ business_date: Optional[str] = None,
63
+ page: int = 1, size: int = 20,
64
+ sort_by: str = "contractId",
65
+ sort_order: str = "asc") -> dict:
66
+ """Per-security Floor Sheet tab (company modal).
67
+
68
+ Mirrors the site's own tab request:
69
+ ``POST /api/nots/security/floorsheet/{id}?[&businessDate=]...``.
70
+ ``business_date`` defaults to the market ``asOf`` day like the UI.
71
+
72
+ NOTE: the server currently answers nginx ``403 Forbidden`` to this
73
+ request — including requests made by the genuine site itself —
74
+ so this method exists for the day they fix it server-side. Until
75
+ then it raises ``AuthExpiredError("access rejected with HTTP 403")``
76
+ after the normal single refresh + retry. The checksum variant is
77
+ likewise unverified; it follows the sibling floorsheet endpoint
78
+ (variant C).
79
+ """
80
+ if business_date is None:
81
+ business_date = str(self.market_status().get("asOf", ""))[:10]
82
+ query = f"?businessDate={business_date}&size={size}"
83
+ query += f"&sort={sort_by},{sort_order}"
84
+ if page > 1:
85
+ query = query.replace("?", f"?page={page - 1}", 1)
86
+ return self.post_json( # type: ignore[return-value]
87
+ f"/api/nots/security/floorsheet/{security_id}{query}",
88
+ {"id": self._signed("c")})
89
+
90
+ # -- signed POST: index graphs -----------------------------------------------
91
+ def index_intraday(self, index_id: int = 58) -> list:
92
+ """Intraday index chart [[epochSeconds, value]] (homepage graph)."""
93
+ return self.post_json(f"/api/nots/graph/index/{index_id}", # type: ignore[return-value]
94
+ {"id": self._signed("a")})
95
+
96
+ def index_range(self, index_code: int, start: str, end: str) -> list:
97
+ """Index chart over a range (Charts 1W/1M/1Q/1Y; dates 'yyyy-MM-dd')."""
98
+ return self.post_json( # type: ignore[return-value]
99
+ f"/api/nots/graph/index?indexCode={index_code}"
100
+ f"&startDate={start}&endDate={end}",
101
+ {"id": self._signed("a")})
nepsepy/_securities.py ADDED
@@ -0,0 +1,111 @@
1
+ """Securities directory and listings. endpoints (mixin for NepseClient)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Optional
6
+
7
+
8
+ class SecuritiesMixin:
9
+ # -- securities directory ----------------------------------------------
10
+ def securities(self) -> list:
11
+ """All securities [{securityId, securityName, securitySymbol}]."""
12
+ return self.get_json("/api/nots/securities") # type: ignore[return-value]
13
+
14
+ def companies(self) -> list:
15
+ """Non-delisted companies (selectors site-wide)."""
16
+ return self.get_json("/api/nots/security?nonDelisted=true") # type: ignore[return-value]
17
+
18
+ def companies_list(self) -> list:
19
+ """Listed companies directory."""
20
+ return self.get_json("/api/nots/company/list") # type: ignore[return-value]
21
+
22
+ def companies_paged(self, page: int = 1) -> dict:
23
+ """Combined directory {companies: Spring page, securities: 938 flat}.
24
+
25
+ (Site's getListsOfCompanies(); page 1-based like the UI.)"""
26
+ path = "/api/nots/company/"
27
+ if page > 1:
28
+ path += f"?page={page - 1}"
29
+ return self.get_json(path) # type: ignore[return-value]
30
+
31
+ def margin_companies(self) -> list:
32
+ """Margin-tradable companies."""
33
+ return self.get_json("/api/nots/company/margin-list") # type: ignore[return-value]
34
+
35
+ def sectors(self) -> dict:
36
+ """Sector directory {sectors[{id, sectorDescription, regulatoryBody}]}."""
37
+ return self.get_json("/api/nots/sector") # type: ignore[return-value]
38
+
39
+ def debentures(self, instrument_type: str = "govBonds") -> list:
40
+ """Listed debentures/bonds (Listing menu page; ``?type=`` required).
41
+
42
+ Verified against the live page dropdown: ``"govBonds"`` (default,
43
+ 68 government bonds) and ``"debenture"`` (77 corporate debentures).
44
+ Unknown values fall back to the bond list server-side.
45
+ """
46
+ return self.get_json( # type: ignore[return-value]
47
+ f"/api/nots/company/debentureAndBond?type={instrument_type}")
48
+
49
+ def companies_non_promoter(self) -> list:
50
+ """Non-promoter securities [{id, symbol, securityName, ...}].
51
+
52
+ Filter source on the Listed Securities / Margin Securities pages
53
+ (``security?nonPromoter=true`` — 649 rows, flatter than companies()).
54
+ """
55
+ return self.get_json("/api/nots/security?nonPromoter=true") # type: ignore[return-value]
56
+
57
+ def share_groups(self) -> list:
58
+ """Share groups [{id, name}] — names A/Z/B/G drive classification()."""
59
+ return self.get_json("/api/nots/security/shareGroup/") # type: ignore[return-value]
60
+
61
+ def classification(self, page: int = 1, size: int = 20,
62
+ share_group: Optional[str] = None) -> dict:
63
+ """Company Classification page (Spring page; page 1-based like the UI;
64
+ share_group is the A/Z/B/G letter — verified exhaustive: 31/107/108/34)."""
65
+ query = f"?&size={size}"
66
+ if share_group is not None:
67
+ query += f"&shareGroup={share_group}"
68
+ if page > 1:
69
+ query = query.replace("?", f"?page={page - 1}", 1)
70
+ # type: ignore[return-value]
71
+ return self.get_json(f"/api/nots/security/classification{query}")
72
+
73
+ def promoters(self, page: int = 1, size: int = 20) -> dict:
74
+ """Promoter Share page (Spring page, 289 rows; page 1-based)."""
75
+ path = "/api/nots/security/promoters?&size=" + str(size)
76
+ if page > 1:
77
+ path = f"/api/nots/security/promoters?page={page - 1}&size={size}"
78
+ return self.get_json(path) # type: ignore[return-value]
79
+
80
+ def report_types(self) -> list:
81
+ """Report directory (Reports menu: Weekly/Monthly/Annual/AGM/Other)."""
82
+ return self.get_json("/api/nots/report/report-types") # type: ignore[return-value]
83
+
84
+ def reports_by_category(self, category_id: int, page: int = 1,
85
+ size: int = 20) -> dict:
86
+ """Report files by category (Reports pages; category id = report
87
+ type id 1..5; page 1-based like the UI; category 5 is empty)."""
88
+ path = f"/api/web/report/reportByCategory/{category_id}?"
89
+ if page > 1 or size != 20:
90
+ path = (f"/api/web/report/reportByCategory/{category_id}?"
91
+ f"page={page - 1}&size={size}")
92
+ return self.get_json(path) # type: ignore[return-value]
93
+
94
+ def events(self, page: int = 1, size: int = 20) -> dict:
95
+ """Events page (Spring page; site grid uses size=6)."""
96
+ path = "/api/web/event?&size=" + str(size)
97
+ if page > 1:
98
+ path = f"/api/web/event?page={page - 1}&size={size}"
99
+ return self.get_json(path) # type: ignore[return-value]
100
+
101
+ def margin_trades(self, business_date: Optional[str] = None) -> dict:
102
+ """Margin Trades page ({content Spring page, totalAmountInvested}).
103
+
104
+ ``business_date`` defaults to the market ``asOf`` day. NOTE: the
105
+ site itself calls this endpoint with an empty query and gets HTTP
106
+ 500 back; ``?businessDate=`` answers 200 (empty as of 2026-09-24).
107
+ """
108
+ if business_date is None:
109
+ business_date = str(self.market_status().get("asOf", ""))[:10]
110
+ return self.get_json( # type: ignore[return-value]
111
+ f"/api/margin/trades-report?businessDate={business_date}")