diff --git a/README.md b/README.md index a99bc71..5c06642 100644 --- a/README.md +++ b/README.md @@ -12,13 +12,14 @@ You can install this library using the following command: You can find examples of using each component in `examples` directory. -| Component | Example File | -|--------------|-------------------------------------------------------------| -| Symbol | [symbol_example.py](examples/symbol_example.py) | -| Market Watch | [market_watch_example.py](examples/market_watch_example.py) | -| Day Details | [day_details_example.py](examples/day_details_example.py) | -| Market Map | [market_map_example.py](examples/market_map_example.py) | -| Group | [group_example.py](examples/group_example.py) | +| Component | Example File | +|--------------|--------------------------------------------------------------------------------------------------------------| +| Symbol | [symbol_example.py](https://github.com/mahs4d/tsetmc-api/blob/master/examples/symbol_example.py) | +| Market Watch | [market_watch_example.py](https://github.com/mahs4d/tsetmc-api/blob/master/examples/market_watch_example.py) | +| Day Details | [day_details_example.py](https://github.com/mahs4d/tsetmc-api/blob/master/examples/day_details_example.py) | +| Market Map | [market_map_example.py](https://github.com/mahs4d/tsetmc-api/blob/master/examples/market_map_example.py) | +| Group | [group_example.py](https://github.com/mahs4d/tsetmc-api/blob/master/examples/group_example.py) | +| Async | [async_example.py](https://github.com/mahs4d/tsetmc-api/blob/master/examples/async_example.py) | ## Usage @@ -54,6 +55,11 @@ Group component currently only has one function (`get_all_groups`) which returns Tsetmc sometimes returns 403 and you should retry. +### Async Support + +Each method in the library has an async counterpart with the same name and a `_async` suffix. +These methods use the sync functions behind the scene in an asyncio pool executor. + ### TODO - [ ] Migrate `symbol` component to use new tsetmc. @@ -61,7 +67,7 @@ Tsetmc sometimes returns 403 and you should retry. - [x] Migrate `day_details` component to use new tsetmc. - [x] Migrate `market_map` component to use new tsetmc. - [x] Migrate `group` component to use new tsetmc. -- [ ] Support asyncio. +- [x] Support asyncio. ## Support and Donation diff --git a/examples/async_example.py b/examples/async_example.py new file mode 100644 index 0000000..e4ecc9f --- /dev/null +++ b/examples/async_example.py @@ -0,0 +1,21 @@ +import asyncio + +from tsetmc_api.symbol import Symbol + + +async def main(): + # دیدن مشخصات یک نماد (نماد آسیاتک) + # این عددی که به عنوان symbol_id بهش میدیم، از تیکه‌ی آخر url صفحه‌ی آسیاتک توی سایت برداشته شده + # مثلا آدرس آسیاتک توی tsetmc اینه: http://main.tsetmc.com/InstInfo/14079693677610396 + # اون تیکه آخرش میشه symbol_id + symbol = Symbol(symbol_id='14079693677610396') + + # اطلاعات قیمتی داخل در یک نگاه + # نام تابع دقیقا همان نام تابع sync است با یک پسوند _async + price_overview = await symbol.get_price_overview_async() + print(price_overview) + + +if __name__ == '__main__': + loop = asyncio.new_event_loop() + loop.run_until_complete(main()) diff --git a/lib/tsetmc_api/day_details/_core.py b/lib/tsetmc_api/day_details/_core.py index 755f612..ebba204 100644 --- a/lib/tsetmc_api/day_details/_core.py +++ b/lib/tsetmc_api/day_details/_core.py @@ -227,7 +227,7 @@ def get_day_details_shareholders_data(symbol_id: str, date: jdate) -> tuple[list 'shares_percentage': row['perOfShares'], } - if row['dEven'] < it: + if row['dEven'] <= it: old_shareholders.append(sh_data) else: new_shareholders.append(sh_data) diff --git a/lib/tsetmc_api/day_details/day_details.py b/lib/tsetmc_api/day_details/day_details.py index 71890fd..80eaa90 100644 --- a/lib/tsetmc_api/day_details/day_details.py +++ b/lib/tsetmc_api/day_details/day_details.py @@ -7,6 +7,7 @@ from .threshold import DayDetailsThresholdsData from .trade import DayDetailsTradeDataRow from .traders_type import DayDetailsTradersTypeData, DayDetailsTradersTypeInfo, DayDetailsTradersTypeSubInfo +from ..utils import run_sync_function class DayDetails: @@ -34,6 +35,15 @@ def get_price_overview(self) -> DayDetailsPriceOverview: value=raw_data['value'], ) + async def get_price_overview_async(self) -> DayDetailsPriceOverview: + """ + returns an overview of price information for that day + """ + + return await run_sync_function( + func=self.get_price_overview, + ) + def get_price_data(self) -> list[DayDetailsPriceDataRow]: """ returns instant prices (for each time in that date) @@ -50,6 +60,15 @@ def get_price_data(self) -> list[DayDetailsPriceDataRow]: count=row['count'], ) for row in raw_data] + async def get_price_data_async(self) -> list[DayDetailsPriceDataRow]: + """ + returns instant prices (for each time in that date) + """ + + return await run_sync_function( + func=self.get_price_data, + ) + def get_orderbook_data(self) -> list[DayDetailsOrderBookDataRow]: """ returns instant orderbooks (for each time in that date) @@ -73,6 +92,15 @@ def get_orderbook_data(self) -> list[DayDetailsOrderBookDataRow]: ) for row in data['sell_rows']], ) for data in raw_data] + async def get_orderbook_data_async(self) -> list[DayDetailsOrderBookDataRow]: + """ + returns instant orderbooks (for each time in that date) + """ + + return await run_sync_function( + func=self.get_orderbook_data, + ) + def get_traders_type_data(self) -> DayDetailsTradersTypeData: """ returns traders type information for that day @@ -107,6 +135,15 @@ def get_traders_type_data(self) -> DayDetailsTradersTypeData: ), ) + async def get_traders_type_data_async(self) -> DayDetailsTradersTypeData: + """ + returns traders type information for that day + """ + + return await run_sync_function( + func=self.get_traders_type_data, + ) + def get_trades_data(self, summarize: bool = False) -> list[DayDetailsTradeDataRow]: """ gets all trade data @@ -120,7 +157,21 @@ def get_trades_data(self, summarize: bool = False) -> list[DayDetailsTradeDataRo volume=row['volume'], ) for row in raw_data] + async def get_trades_data_async(self, summarize: bool = False) -> list[DayDetailsTradeDataRow]: + """ + gets all trade data + """ + + return await run_sync_function( + func=self.get_trades_data, + summarize=summarize, + ) + def get_thresholds_data(self) -> list[DayDetailsThresholdsData]: + """ + Get allowed price window + """ + raw_data = _core.get_day_details_thresholds_data(symbol_id=self.symbol_id, date=self.date) return [DayDetailsThresholdsData( @@ -129,6 +180,15 @@ def get_thresholds_data(self) -> list[DayDetailsThresholdsData]: range_min=row['min'], ) for row in raw_data] + async def get_thresholds_data_async(self) -> list[DayDetailsThresholdsData]: + """ + Get allowed price window + """ + + return await run_sync_function( + func=self.get_thresholds_data, + ) + def get_shareholders_data(self) -> tuple[list[DayDetailsShareHolderDataRow], list[DayDetailsShareHolderDataRow]]: """ gets list of shareholders before and after the day @@ -143,8 +203,8 @@ def get_shareholders_data(self) -> tuple[list[DayDetailsShareHolderDataRow], lis symbol_id=self.symbol_id, date=self.date, shareholder=DayDetailsShareHolder(id=row['id'], name=row['name']), - count=row['count'], - percentage=row['percentage'], + shares_count=row['shares_count'], + shares_percentage=row['shares_percentage'], ) for row in raw_old_shareholders] new_shareholders = [DayDetailsShareHolderDataRow( @@ -156,3 +216,15 @@ def get_shareholders_data(self) -> tuple[list[DayDetailsShareHolderDataRow], lis ) for row in raw_new_shareholders] return old_shareholders, new_shareholders + + async def get_shareholders_data_async(self) -> tuple[ + list[DayDetailsShareHolderDataRow], + list[DayDetailsShareHolderDataRow], + ]: + """ + gets list of shareholders before and after the day + """ + + return await run_sync_function( + func=self.get_shareholders_data, + ) diff --git a/lib/tsetmc_api/day_details/shareholder.py b/lib/tsetmc_api/day_details/shareholder.py index 7f1f849..8f34987 100644 --- a/lib/tsetmc_api/day_details/shareholder.py +++ b/lib/tsetmc_api/day_details/shareholder.py @@ -2,6 +2,7 @@ from pydantic import BaseModel from . import _core +from ..utils import run_sync_function class DayDetailsShareHolderPortfolioRow(BaseModel): @@ -31,6 +32,15 @@ def get_portfolio_data(self) -> list[DayDetailsShareHolderPortfolioRow]: shares_percent=row['shares_percent'], ) for row in raw_data] + async def get_portfolio_data_async(self) -> list[DayDetailsShareHolderPortfolioRow]: + """ + returns list of companies owned by this shareholder + """ + + return await run_sync_function( + func=self.get_portfolio_data, + ) + class DayDetailsShareHolderChartRow(BaseModel): date: jdate @@ -65,5 +75,15 @@ def get_chart_data(self, days: int = 90) -> list[DayDetailsShareHolderChartRow]: shares_percentage=row['shares_percentage'], ) for row in raw_data] + async def get_chart_data_async(self, days: int = 90) -> list[DayDetailsShareHolderChartRow]: + """ + returns list of changes to shareholders share count in history of this symbol + """ + + return await run_sync_function( + func=self.get_chart_data_async, + days=days, + ) + class Config: arbitrary_types_allowed = True diff --git a/lib/tsetmc_api/group/group.py b/lib/tsetmc_api/group/group.py index 74b11f8..082ec82 100644 --- a/lib/tsetmc_api/group/group.py +++ b/lib/tsetmc_api/group/group.py @@ -5,6 +5,7 @@ from pydantic import BaseModel from . import _core +from ..utils import run_sync_function class GroupType(Enum): @@ -33,3 +34,13 @@ def get_all_groups() -> list[Group]: description=row['description'], type=GroupType.PAPER if row['type'] == 'PaperType' else GroupType.INDUSTRIAL, ) for row in raw_data] + + @staticmethod + async def get_all_groups_async() -> list[Group]: + """ + returns list of symbol groups + """ + + return await run_sync_function( + func=Group.get_all_groups, + ) diff --git a/lib/tsetmc_api/market_map/map.py b/lib/tsetmc_api/market_map/map.py index f0f22ac..75abc8c 100644 --- a/lib/tsetmc_api/market_map/map.py +++ b/lib/tsetmc_api/market_map/map.py @@ -4,6 +4,7 @@ from pydantic.utils import deep_update from . import _core +from ..utils import run_sync_function class MapDataRow(BaseModel): @@ -64,3 +65,14 @@ def get_market_map_data(self, map_type: MapType = MapType.MARKET_VALUE) -> dict[ ) for key, data in raw_data.items()} return map_data + + async def get_market_map_data_async(self, map_type: MapType = MapType.MARKET_VALUE) -> dict[str, MapDataRow]: + """ + returns symbol data in market map (in "naghshe bazar" page) + !!! webserver occasionally throws 403 error, you should retry in a few seconds when this happens + """ + + return await run_sync_function( + func=self.get_market_map_data, + map_type=map_type, + ) diff --git a/lib/tsetmc_api/market_watch/_core.py b/lib/tsetmc_api/market_watch/_core.py index 518c2cd..9913d0b 100644 --- a/lib/tsetmc_api/market_watch/_core.py +++ b/lib/tsetmc_api/market_watch/_core.py @@ -267,7 +267,7 @@ def get_watch_traders_type_data() -> dict: def get_watch_daily_history_data() -> dict: response = requests.get( - url='http://members.tsetmc.com/tsev2/data/ClosingPriceAll.aspx', + url='https://members.tsetmc.com/tsev2/data/ClosingPriceAll.aspx', params={}, verify=False, timeout=20, diff --git a/lib/tsetmc_api/market_watch/watch.py b/lib/tsetmc_api/market_watch/watch.py index 4cff460..ec3df54 100644 --- a/lib/tsetmc_api/market_watch/watch.py +++ b/lib/tsetmc_api/market_watch/watch.py @@ -3,7 +3,7 @@ from .orderbook import WatchOrderBook, WatchOrderBookRow from .price import WatchPriceDataRow from .traders_type import WatchTradersTypeDataRow, WatchTradersTypeInfo, WatchTradersTypeSubInfo -from ..utils import deep_update +from ..utils import deep_update, run_sync_function class MarketWatch: @@ -71,6 +71,15 @@ def get_price_data(self) -> dict[str, WatchPriceDataRow]: return watch_data + async def get_price_data_async(self) -> dict[str, WatchPriceDataRow]: + """ + gets basic price information (in "didbane bazar" page) + """ + + return await run_sync_function( + func=self.get_price_data, + ) + def get_traders_type_data(self) -> dict[str, WatchTradersTypeDataRow]: """ gets traders type data (in "didebane bazar" page) @@ -105,6 +114,15 @@ def get_traders_type_data(self) -> dict[str, WatchTradersTypeDataRow]: return watch_data + async def get_traders_type_data_async(self) -> dict[str, WatchTradersTypeDataRow]: + """ + gets traders type data (in "didebane bazar" page) + """ + + return await run_sync_function( + func=self.get_traders_type_data, + ) + def get_daily_history_data(self) -> dict[str, list[WatchDailyHistoryDataRow]]: """ gets 30 day history of symbols (in "didbane bazar" page) @@ -129,14 +147,43 @@ def get_daily_history_data(self) -> dict[str, list[WatchDailyHistoryDataRow]]: return watch_data + async def get_daily_history_data_async(self) -> dict[str, list[WatchDailyHistoryDataRow]]: + """ + gets 30 day history of symbols (in "didbane bazar" page) + """ + + return await run_sync_function( + func=self.get_daily_history_data, + ) + def get_raw_stats_data(self) -> dict[list]: """ returns a list of stats for each symbol. refer to tsetmc.com for information of what each item in the list is """ + return _core.get_watch_raw_stats_data() + + async def get_raw_stats_data_async(self) -> dict[list]: + """ + returns a list of stats for each symbol. refer to tsetmc.com for information of what each item in the list is + """ + + return await run_sync_function( + func=self.get_raw_stats_data, + ) + def get_stats_data(self) -> dict[dict]: """ !!! EXPERIMENTAL: returns stats in dict, may be wrong !!! """ return _core.get_watch_stats_data() + + async def get_stats_data_async(self) -> dict[dict]: + """ + !!! EXPERIMENTAL: returns stats in dict, may be wrong !!! + """ + + return await run_sync_function( + func=self.get_stats_data, + ) diff --git a/lib/tsetmc_api/symbol/_core.py b/lib/tsetmc_api/symbol/_core.py index e21cbd2..4849914 100644 --- a/lib/tsetmc_api/symbol/_core.py +++ b/lib/tsetmc_api/symbol/_core.py @@ -393,7 +393,7 @@ def get_symbol_shareholders(company_isin: str) -> list[dict]: name = tds[0].text.strip() count = int(tds[1].div['title'].replace(',', '')) percentage = float(tds[2].text) - change = locale.atoi(tds[3].text.strip()) + change = locale.atoi(tds[3].text.replace(',', '').strip()) shareholders.append({ 'id': shareholder_id, diff --git a/lib/tsetmc_api/symbol/shareholder.py b/lib/tsetmc_api/symbol/shareholder.py index d90d4be..2a50f2b 100644 --- a/lib/tsetmc_api/symbol/shareholder.py +++ b/lib/tsetmc_api/symbol/shareholder.py @@ -4,6 +4,7 @@ from pydantic import BaseModel, PrivateAttr from . import _core +from ..utils import run_sync_function class SymbolShareHolderPortfolioRow(BaseModel): @@ -39,6 +40,15 @@ def get_portfolio_data(self) -> list[SymbolShareHolderPortfolioRow]: shares_percentage=row['shares_percentage'], ) for row in raw_data] + async def get_portfolio_data_async(self) -> list[SymbolShareHolderPortfolioRow]: + """ + returns list of companies owned by this shareholder + """ + + return await run_sync_function( + func=self.get_portfolio_data, + ) + class SymbolShareHolderChartRow(BaseModel): date: jdate @@ -68,3 +78,12 @@ def get_chart_data(self) -> list[SymbolShareHolderChartRow]: date=row['date'], shares_count=row['shares_count'], ) for row in raw_data] + + async def get_chart_data_async(self) -> list[SymbolShareHolderChartRow]: + """ + returns list of changes to shareholders share count in history of this symbol + """ + + return await run_sync_function( + func=self.get_chart_data, + ) diff --git a/lib/tsetmc_api/symbol/symbol.py b/lib/tsetmc_api/symbol/symbol.py index baaf179..e775f78 100644 --- a/lib/tsetmc_api/symbol/symbol.py +++ b/lib/tsetmc_api/symbol/symbol.py @@ -8,6 +8,7 @@ from .state_change import SymbolStateChangeDataRow from .supervisor_message import SymbolSupervisorMessageDataRow from .traders_type import SymbolTradersTypeDataRow, SymbolTradersTypeInfo, SymbolTradersTypeSubInfo +from ..utils import run_sync_function class Symbol: @@ -93,6 +94,15 @@ def get_price_overview(self) -> SymbolPriceOverview: group_data=group_data, ) + async def get_price_overview_async(self) -> SymbolPriceOverview: + """ + gets the last price overview of the symbol and returns most of the information (in "dar yek negah" tab) + """ + + return await run_sync_function( + func=self.get_price_overview, + ) + def get_intraday_price_chart_data(self) -> list[SymbolIntraDayPriceChartDataRow]: """ gets last days intraday price chart (in "dar yek negah" tab) @@ -111,6 +121,15 @@ def get_intraday_price_chart_data(self) -> list[SymbolIntraDayPriceChartDataRow] return ticks + async def get_intraday_price_chart_data_async(self) -> list[SymbolIntraDayPriceChartDataRow]: + """ + gets last days intraday price chart (in "dar yek negah" tab) + """ + + return await run_sync_function( + func=self.get_intraday_price_chart_data, + ) + def get_supervisor_messages_data(self) -> list[SymbolSupervisorMessageDataRow]: """ get list of supervisor messages (in "payame nazer" tab) @@ -126,6 +145,15 @@ def get_supervisor_messages_data(self) -> list[SymbolSupervisorMessageDataRow]: return messages + async def get_supervisor_messages_data_async(self) -> list[SymbolSupervisorMessageDataRow]: + """ + get list of supervisor messages (in "payame nazer" tab) + """ + + return await run_sync_function( + func=self.get_supervisor_messages_data, + ) + def get_notifications_data(self) -> list[SymbolNotificationsDataRow]: """ get list of notifications (in "etelaiye ha" tab) @@ -140,6 +168,15 @@ def get_notifications_data(self) -> list[SymbolNotificationsDataRow]: return notifications + async def get_notifications_data_async(self) -> list[SymbolNotificationsDataRow]: + """ + get list of notifications (in "etelaiye ha" tab) + """ + + return await run_sync_function( + func=self.get_notifications_data, + ) + def get_state_changes_data(self) -> list[SymbolStateChangeDataRow]: """ get list of state changes (in "taghire vaziat" tab) @@ -154,6 +191,15 @@ def get_state_changes_data(self) -> list[SymbolStateChangeDataRow]: return state_changes + async def get_state_changes_data_async(self) -> list[SymbolStateChangeDataRow]: + """ + get list of state changes (in "taghire vaziat" tab) + """ + + return await run_sync_function( + func=self.get_state_changes_data, + ) + def get_daily_history(self) -> list[SymbolDailyPriceDataRow]: """ get list of daily ticks history (in "sabeghe" tab) @@ -176,6 +222,15 @@ def get_daily_history(self) -> list[SymbolDailyPriceDataRow]: return ticks + async def get_daily_history_async(self) -> list[SymbolDailyPriceDataRow]: + """ + get list of daily ticks history (in "sabeghe" tab) + """ + + return await run_sync_function( + func=self.get_daily_history, + ) + def get_id_details(self) -> SymbolIdDetails: """ gets symbol identity details and returns all the information (in "shenase" tab) @@ -206,6 +261,15 @@ def get_id_details(self) -> SymbolIdDetails: return details + async def get_id_details_async(self) -> SymbolIdDetails: + """ + gets symbol identity details and returns all the information (in "shenase" tab) + """ + + return await run_sync_function( + func=self.get_id_details, + ) + def get_traders_type_history(self) -> list[SymbolTradersTypeDataRow]: """ returns daily traders type history (in "haghihi-hoghooghi" tab) @@ -243,6 +307,15 @@ def get_traders_type_history(self) -> list[SymbolTradersTypeDataRow]: return traders_type_history + async def get_traders_type_history_async(self) -> list[SymbolTradersTypeDataRow]: + """ + returns daily traders type history (in "haghihi-hoghooghi" tab) + """ + + return await run_sync_function( + func=self.get_traders_type_history, + ) + def get_shareholders_data(self) -> list[SymbolShareHolderDataRow]: """ returns list of major shareholders (in "saham daran" tab) @@ -263,3 +336,12 @@ def get_shareholders_data(self) -> list[SymbolShareHolderDataRow]: ) for row in raw_data] return shareholders + + async def get_shareholders_data_async(self) -> list[SymbolShareHolderDataRow]: + """ + returns list of major shareholders (in "saham daran" tab) + """ + + return await run_sync_function( + func=self.get_shareholders_data, + ) diff --git a/lib/tsetmc_api/utils.py b/lib/tsetmc_api/utils.py index 45f8de7..d65ac41 100644 --- a/lib/tsetmc_api/utils.py +++ b/lib/tsetmc_api/utils.py @@ -1,4 +1,7 @@ +import asyncio from copy import deepcopy +from functools import partial +from typing import Callable, Any from jdatetime import date as jdate, time as jtime @@ -34,3 +37,11 @@ def convert_deven_to_jdate(deven: int) -> jdate: month=int(deven[4:6]), day=int(deven[6:]), ) + + +async def run_sync_function(func: Callable, *args, **kwargs) -> Any: + loop = asyncio.get_event_loop() + return await loop.run_in_executor( + executor=None, + func=partial(func, *args, **kwargs), + ) diff --git a/lib/tsetmc_api/version.py b/lib/tsetmc_api/version.py index d741750..7342798 100644 --- a/lib/tsetmc_api/version.py +++ b/lib/tsetmc_api/version.py @@ -1 +1 @@ -VERSION = '5.2.0' +VERSION = '5.3.0' diff --git a/pyproject.toml b/pyproject.toml index 7204d14..7f66b6a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "tsetmc-api" -version = "5.2.0" +version = "5.3.0" description = "simple package to communicate and crawl data from tsetmc.com (Tehran Stock Exchange Website)" license = "MIT" repository = "https://github.com/mahs4d/tsetmc-api"