finance_dev 0.9.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.
- finance_dev/__init__.py +18 -0
- finance_dev/_hooks/__init__.py +7 -0
- finance_dev/_hooks/clientcredentials.py +319 -0
- finance_dev/_hooks/oauth2scopes.py +50 -0
- finance_dev/_hooks/registration.py +13 -0
- finance_dev/_hooks/sdkhooks.py +82 -0
- finance_dev/_hooks/types.py +125 -0
- finance_dev/_version.py +16 -0
- finance_dev/accounts.py +797 -0
- finance_dev/alternativedata.py +4830 -0
- finance_dev/analytics.py +3757 -0
- finance_dev/basesdk.py +393 -0
- finance_dev/calendar.py +1523 -0
- finance_dev/chart.py +1881 -0
- finance_dev/company.py +1105 -0
- finance_dev/corporateactions.py +1795 -0
- finance_dev/economy.py +1153 -0
- finance_dev/errors/__init__.py +94 -0
- finance_dev/errors/apierror.py +41 -0
- finance_dev/errors/authenticationerrorenvelope.py +43 -0
- finance_dev/errors/badrequest.py +34 -0
- finance_dev/errors/errorenvelope.py +42 -0
- finance_dev/errors/errorresponse.py +45 -0
- finance_dev/errors/financedevapierror.py +31 -0
- finance_dev/errors/forbidden.py +34 -0
- finance_dev/errors/internalservererror.py +35 -0
- finance_dev/errors/no_response_error.py +18 -0
- finance_dev/errors/notfound.py +34 -0
- finance_dev/errors/orderhistorybadrequest.py +35 -0
- finance_dev/errors/predorderserrorenvelope.py +42 -0
- finance_dev/errors/responsevalidationerror.py +28 -0
- finance_dev/errors/unauthorized.py +34 -0
- finance_dev/estimates.py +1906 -0
- finance_dev/etf.py +1108 -0
- finance_dev/filings.py +586 -0
- finance_dev/financials.py +4079 -0
- finance_dev/history.py +477 -0
- finance_dev/httpclient.py +126 -0
- finance_dev/indices.py +2308 -0
- finance_dev/logos.py +692 -0
- finance_dev/market.py +1825 -0
- finance_dev/markets.py +952 -0
- finance_dev/models/__init__.py +4568 -0
- finance_dev/models/accountbalancev2.py +340 -0
- finance_dev/models/accountitem.py +78 -0
- finance_dev/models/accountposition.py +147 -0
- finance_dev/models/aggregatetechnicalindicatorsresponse.py +192 -0
- finance_dev/models/analystinsightitem.py +161 -0
- finance_dev/models/analystinsightsresponse.py +39 -0
- finance_dev/models/apperror.py +123 -0
- finance_dev/models/authenticationerrordetail.py +58 -0
- finance_dev/models/bankbranchesresponse.py +96 -0
- finance_dev/models/basicfinancials.py +243 -0
- finance_dev/models/bullbearanalysisresponse.py +150 -0
- finance_dev/models/cancelorderop.py +40 -0
- finance_dev/models/cancelordrequest_union.py +287 -0
- finance_dev/models/cancelordresponse.py +115 -0
- finance_dev/models/cashflowstatement.py +290 -0
- finance_dev/models/cashflowstatementsresponse.py +61 -0
- finance_dev/models/chartbars.py +111 -0
- finance_dev/models/chartconfig.py +81 -0
- finance_dev/models/chartresolution.py +46 -0
- finance_dev/models/companyesgresponse.py +408 -0
- finance_dev/models/companyexecutive.py +70 -0
- finance_dev/models/companyexecutiveresponse.py +39 -0
- finance_dev/models/companypeersresponse.py +38 -0
- finance_dev/models/companypresentationresponse.py +96 -0
- finance_dev/models/companyprofile.py +255 -0
- finance_dev/models/conferencecallsresponse.py +169 -0
- finance_dev/models/congressionaltradingresponse.py +138 -0
- finance_dev/models/covid19uscasesresponse.py +79 -0
- finance_dev/models/createconfigwatchlistop.py +40 -0
- finance_dev/models/createwatchlistrequest.py +43 -0
- finance_dev/models/dailymarketsummaryresponse.py +73 -0
- finance_dev/models/deleteconfigwatchlistbyidop.py +53 -0
- finance_dev/models/dividenditem.py +125 -0
- finance_dev/models/dividendsresponse.py +61 -0
- finance_dev/models/earningrelease.py +291 -0
- finance_dev/models/earningscallliveresponse.py +103 -0
- finance_dev/models/earningsqualityscoreresponse.py +140 -0
- finance_dev/models/earningssurprisesresponse.py +125 -0
- finance_dev/models/ebitdaestimatesresponse.py +176 -0
- finance_dev/models/ebitestimatesresponse.py +176 -0
- finance_dev/models/economicscalendarevent.py +160 -0
- finance_dev/models/economicsindicator.py +87 -0
- finance_dev/models/epsestimatesresponse.py +176 -0
- finance_dev/models/equitydetailsdto.py +569 -0
- finance_dev/models/errorcode.py +48 -0
- finance_dev/models/etblistresponse.py +43 -0
- finance_dev/models/etbsearchrequest.py +21 -0
- finance_dev/models/etbsearchresponse.py +43 -0
- finance_dev/models/etfcountryallocationresponse.py +113 -0
- finance_dev/models/etfholdingsresponse.py +163 -0
- finance_dev/models/etfprofileresponse.py +234 -0
- finance_dev/models/etfsectorallocationresponse.py +113 -0
- finance_dev/models/exchangecode.py +90 -0
- finance_dev/models/fdaadvisorycommitteecalendarresponse.py +88 -0
- finance_dev/models/filingsimilarityrecord.py +115 -0
- finance_dev/models/filingssimilarityindexdatawrapper.py +52 -0
- finance_dev/models/filingssimilarityindexresponse.py +42 -0
- finance_dev/models/financialratiosresponse.py +252 -0
- finance_dev/models/financialsreporteddatawrapper.py +49 -0
- finance_dev/models/financialsreportedresponse.py +42 -0
- finance_dev/models/financialsrevenuebreakdowndatawrapper.py +49 -0
- finance_dev/models/financialsrevenuebreakdownresponse.py +42 -0
- finance_dev/models/financialsstatementsdatawrapper.py +44 -0
- finance_dev/models/financialsstatementsresponse.py +42 -0
- finance_dev/models/financialstatement.py +171 -0
- finance_dev/models/financialstatementtype.py +14 -0
- finance_dev/models/getaccountbalanceop.py +53 -0
- finance_dev/models/getaccountpositionsop.py +53 -0
- finance_dev/models/getaccountsresponse.py +22 -0
- finance_dev/models/getaggregatetechnicalindicatorsop.py +94 -0
- finance_dev/models/getanalystinsightsop.py +164 -0
- finance_dev/models/getbankbranchesop.py +53 -0
- finance_dev/models/getbaseeventop.py +87 -0
- finance_dev/models/getbullbearanalysisop.py +91 -0
- finance_dev/models/getcashflowstatementsop.py +564 -0
- finance_dev/models/getchartconfigop.py +40 -0
- finance_dev/models/getcharthistoryop.py +122 -0
- finance_dev/models/getcharttimeop.py +39 -0
- finance_dev/models/getcompanyesgop.py +53 -0
- finance_dev/models/getcompanyexecutivesop.py +56 -0
- finance_dev/models/getcompanymetricsop.py +68 -0
- finance_dev/models/getcompanypeersop.py +53 -0
- finance_dev/models/getcompanypresentationop.py +56 -0
- finance_dev/models/getcompanyprofileop.py +86 -0
- finance_dev/models/getconferencecallsop.py +111 -0
- finance_dev/models/getconfigwatchlistbyidop.py +53 -0
- finance_dev/models/getconfigwatchlistsop.py +40 -0
- finance_dev/models/getcongressionaltradingop.py +73 -0
- finance_dev/models/getcovid19uscasesop.py +43 -0
- finance_dev/models/getdailymarketsummaryop.py +92 -0
- finance_dev/models/getdividendsop.py +294 -0
- finance_dev/models/getearningscalendarop.py +87 -0
- finance_dev/models/getearningscallliveop.py +91 -0
- finance_dev/models/getearningsqualityscoreop.py +81 -0
- finance_dev/models/getearningssurprisesop.py +80 -0
- finance_dev/models/getebitdaestimatesop.py +91 -0
- finance_dev/models/getebitestimatesop.py +88 -0
- finance_dev/models/geteconomicscalendarop.py +92 -0
- finance_dev/models/geteconomicscalendarresponse.py +56 -0
- finance_dev/models/getepsestimatesop.py +88 -0
- finance_dev/models/getequitydetailsop.py +56 -0
- finance_dev/models/getequitydetailsresponse.py +45 -0
- finance_dev/models/getetblistop.py +40 -0
- finance_dev/models/getetfcountryallocationop.py +80 -0
- finance_dev/models/getetfholdingsop.py +96 -0
- finance_dev/models/getetfprofileop.py +77 -0
- finance_dev/models/getetfsectorallocationop.py +80 -0
- finance_dev/models/geteventcontractsop.py +87 -0
- finance_dev/models/getfdaadvisorycommitteecalendarop.py +47 -0
- finance_dev/models/getfilingssimilarityindexop.py +102 -0
- finance_dev/models/getfinancialratiosop.py +612 -0
- finance_dev/models/getfinancialstatementsop.py +117 -0
- finance_dev/models/getfundownershipop.py +77 -0
- finance_dev/models/gethistoricaldataop.py +61 -0
- finance_dev/models/gethistoricalesgop.py +53 -0
- finance_dev/models/gethistorybatchop.py +74 -0
- finance_dev/models/gethistoryop.py +123 -0
- finance_dev/models/getincomestatementsop.py +564 -0
- finance_dev/models/getindexbarop.py +140 -0
- finance_dev/models/getindexbarsop.py +149 -0
- finance_dev/models/getindexchartbarsop.py +178 -0
- finance_dev/models/getindexrealtimevaluesop.py +94 -0
- finance_dev/models/getinflationexpectationsop.py +157 -0
- finance_dev/models/getinflationop.py +154 -0
- finance_dev/models/getinsidertransactionsop.py +90 -0
- finance_dev/models/getinstitutionalownershipop.py +99 -0
- finance_dev/models/getinstitutionalportfolioop.py +75 -0
- finance_dev/models/getinvestmentthemeop.py +68 -0
- finance_dev/models/getipocalendarop.py +63 -0
- finance_dev/models/getlobbyingop.py +70 -0
- finance_dev/models/getmarketbreadthop.py +40 -0
- finance_dev/models/getmarketholidaysop.py +65 -0
- finance_dev/models/getmarketstatusop.py +65 -0
- finance_dev/models/getmergersandacquisitionsop.py +113 -0
- finance_dev/models/getnetinflowop.py +40 -0
- finance_dev/models/getnewsbyidop.py +73 -0
- finance_dev/models/getnewsop.py +98 -0
- finance_dev/models/getnewssentimentop.py +53 -0
- finance_dev/models/getoptionchainsnapshotop.py +244 -0
- finance_dev/models/getoptioncontractsnapshotop.py +69 -0
- finance_dev/models/getoptiondetailsop.py +56 -0
- finance_dev/models/getoptiondetailsresponse.py +45 -0
- finance_dev/models/getoptionlasttradeop.py +56 -0
- finance_dev/models/getoptionopencloseop.py +66 -0
- finance_dev/models/getoptionpreviousdayop.py +56 -0
- finance_dev/models/getoptionquotesop.py +177 -0
- finance_dev/models/getoptionsactivityop.py +111 -0
- finance_dev/models/getoptionsetfstopvolumeop.py +70 -0
- finance_dev/models/getoptionsstockstopgainersop.py +70 -0
- finance_dev/models/getoptionsstockstopvolumeop.py +70 -0
- finance_dev/models/getoptionsunusualactivityop.py +100 -0
- finance_dev/models/getoptiontradesop.py +177 -0
- finance_dev/models/getordersv2response.py +66 -0
- finance_dev/models/getownershipdataop.py +77 -0
- finance_dev/models/getpositionsresponse.py +39 -0
- finance_dev/models/getpredictionorderop.py +88 -0
- finance_dev/models/getpricetargetop.py +53 -0
- finance_dev/models/getquotesop.py +53 -0
- finance_dev/models/getratingsop.py +164 -0
- finance_dev/models/getratingsresponse.py +39 -0
- finance_dev/models/getreportedfinancialsop.py +127 -0
- finance_dev/models/getrevenuebreakdownop.py +85 -0
- finance_dev/models/getrevenueestimatesop.py +91 -0
- finance_dev/models/getsecfilingsop.py +113 -0
- finance_dev/models/getsectortickersop.py +124 -0
- finance_dev/models/getsectorwisechangeop.py +84 -0
- finance_dev/models/getshortinterestop.py +334 -0
- finance_dev/models/getshortvolumeop.py +333 -0
- finance_dev/models/getsocialsentimentop.py +90 -0
- finance_dev/models/getspinoffsop.py +112 -0
- finance_dev/models/getstockaggregatesop.py +174 -0
- finance_dev/models/getstocklastquoteop.py +56 -0
- finance_dev/models/getstocklasttradeop.py +56 -0
- finance_dev/models/getstockopencloseop.py +90 -0
- finance_dev/models/getstockpreviousdayop.py +80 -0
- finance_dev/models/getstockquotesop.py +177 -0
- finance_dev/models/getstocksnapshotsop.py +82 -0
- finance_dev/models/getstocksplitsop.py +243 -0
- finance_dev/models/getstocktradesop.py +177 -0
- finance_dev/models/getsupplychainop.py +53 -0
- finance_dev/models/getsupportresistancelevelsop.py +90 -0
- finance_dev/models/getsymbolinfoop.py +53 -0
- finance_dev/models/gettechnicalindicatorop.py +172 -0
- finance_dev/models/gettechnicalpatternsop.py +88 -0
- finance_dev/models/gettenderoffersop.py +112 -0
- finance_dev/models/gettickhistoryop.py +104 -0
- finance_dev/models/gettopgainersop.py +78 -0
- finance_dev/models/gettoplosersop.py +78 -0
- finance_dev/models/gettopmostactiveop.py +78 -0
- finance_dev/models/gettranscriptop.py +53 -0
- finance_dev/models/gettreasuryyieldsop.py +157 -0
- finance_dev/models/getunifiedsnapshotop.py +223 -0
- finance_dev/models/getupgradesdowngradesop.py +91 -0
- finance_dev/models/getusaspendingop.py +70 -0
- finance_dev/models/getuseraccountsop.py +40 -0
- finance_dev/models/getusptopatentsop.py +70 -0
- finance_dev/models/getvisaapplicationsop.py +73 -0
- finance_dev/models/historicalesgresponse.py +439 -0
- finance_dev/models/httpmetadata.py +24 -0
- finance_dev/models/incomestatement.py +296 -0
- finance_dev/models/incomestatementsresponse.py +61 -0
- finance_dev/models/indexbarresponse.py +194 -0
- finance_dev/models/indexbarsresponse.py +123 -0
- finance_dev/models/indexchartbarsresponse.py +213 -0
- finance_dev/models/indexgroupsresponse.py +78 -0
- finance_dev/models/indexlistresponse.py +95 -0
- finance_dev/models/indexsearchresponse.py +93 -0
- finance_dev/models/indicesbygroupresponse.py +93 -0
- finance_dev/models/indicesidentifiertype.py +28 -0
- finance_dev/models/indicesrealtimeindexinfo.py +67 -0
- finance_dev/models/indicesrealtimeindexvalue.py +157 -0
- finance_dev/models/indicesrealtimevaluesitem.py +80 -0
- finance_dev/models/indicesrealtimevaluesresponse.py +42 -0
- finance_dev/models/inflationexpectationsresponse.py +140 -0
- finance_dev/models/inflationresponse.py +132 -0
- finance_dev/models/insidertransaction.py +97 -0
- finance_dev/models/insidertransactionsresponse.py +39 -0
- finance_dev/models/institutionalholding.py +77 -0
- finance_dev/models/institutionalownershipresponse.py +39 -0
- finance_dev/models/institutionalportfolioresponse.py +39 -0
- finance_dev/models/investmentthemeid.py +35 -0
- finance_dev/models/investmentthemeresponse.py +80 -0
- finance_dev/models/ipocalendarresponse.py +39 -0
- finance_dev/models/ipoitem.py +93 -0
- finance_dev/models/listactiveordersop.py +87 -0
- finance_dev/models/listcategoriesop.py +79 -0
- finance_dev/models/listindexgroupsop.py +40 -0
- finance_dev/models/listindicesbygroupop.py +58 -0
- finance_dev/models/listindicesop.py +91 -0
- finance_dev/models/listoptioncontractsop.py +66 -0
- finance_dev/models/listoptionexpirydatesop.py +56 -0
- finance_dev/models/listorderhistoryop.py +139 -0
- finance_dev/models/listpredictionordersop.py +134 -0
- finance_dev/models/listtranscriptsop.py +53 -0
- finance_dev/models/lobbyingresponse.py +166 -0
- finance_dev/models/logosearchresponse.py +152 -0
- finance_dev/models/logosyncresponse.py +277 -0
- finance_dev/models/marketbreadth.py +97 -0
- finance_dev/models/marketbreadthresponse.py +44 -0
- finance_dev/models/marketholiday.py +56 -0
- finance_dev/models/marketholidayresponse.py +39 -0
- finance_dev/models/marketstatus.py +67 -0
- finance_dev/models/mergersandacquisitionsresponse.py +186 -0
- finance_dev/models/metricseriespoint.py +43 -0
- finance_dev/models/netinflow.py +81 -0
- finance_dev/models/netinflowresponse.py +44 -0
- finance_dev/models/newsitem.py +102 -0
- finance_dev/models/newslistresponse.py +128 -0
- finance_dev/models/newssentimentresponse.py +176 -0
- finance_dev/models/newstypeenum.py +16 -0
- finance_dev/models/optionchainsnapshotresponse.py +64 -0
- finance_dev/models/optioncontractsnapshotresponse.py +64 -0
- finance_dev/models/optioncontractsnapshotrow.py +164 -0
- finance_dev/models/optioncontractsresponse.py +131 -0
- finance_dev/models/optiondetailsdto.py +72 -0
- finance_dev/models/optionexpirydatesresponse.py +53 -0
- finance_dev/models/optionlasttraderesponse.py +169 -0
- finance_dev/models/optionopencloseresponse.py +97 -0
- finance_dev/models/optionpreviousdayresponse.py +89 -0
- finance_dev/models/optionquotesresponse.py +138 -0
- finance_dev/models/optionsactivityresponse.py +265 -0
- finance_dev/models/optionsnapshotcontractdetails.py +85 -0
- finance_dev/models/optionsnapshotdaybar.py +107 -0
- finance_dev/models/optionsnapshotgreeks.py +67 -0
- finance_dev/models/optionsnapshotlastquote.py +116 -0
- finance_dev/models/optionsnapshotlasttrade.py +102 -0
- finance_dev/models/optionsnapshotunderlying.py +86 -0
- finance_dev/models/optionsscreeneritem.py +151 -0
- finance_dev/models/optionsunusualactivityresponse.py +62 -0
- finance_dev/models/optionsunusualactivitytrade.py +103 -0
- finance_dev/models/optiontradesresponse.py +132 -0
- finance_dev/models/orderrequesttimeinforceenum.py +29 -0
- finance_dev/models/ordersideenum.py +20 -0
- finance_dev/models/ordertypeenum.py +20 -0
- finance_dev/models/ordleg.py +162 -0
- finance_dev/models/ordresponse.py +202 -0
- finance_dev/models/ownershipdataresponse.py +39 -0
- finance_dev/models/ownershiprecord.py +74 -0
- finance_dev/models/placeorderop.py +40 -0
- finance_dev/models/placeordrequest_union.py +335 -0
- finance_dev/models/placepredictionorderop.py +114 -0
- finance_dev/models/predmdbaseevent.py +62 -0
- finance_dev/models/predmdbaseeventhit.py +53 -0
- finance_dev/models/predmdbaseeventsearchresult.py +46 -0
- finance_dev/models/predmdbaseeventwithevents.py +32 -0
- finance_dev/models/predmdbatchhistoryrequest.py +16 -0
- finance_dev/models/predmdbatchhistoryresponse.py +16 -0
- finance_dev/models/predmdcategory.py +48 -0
- finance_dev/models/predmdcontract.py +177 -0
- finance_dev/models/predmdcontracthit.py +157 -0
- finance_dev/models/predmdcontractsearchresult.py +46 -0
- finance_dev/models/predmdevent.py +136 -0
- finance_dev/models/predmdeventhit.py +73 -0
- finance_dev/models/predmdeventsearchresult.py +46 -0
- finance_dev/models/predmdeventwithcontracts.py +22 -0
- finance_dev/models/predmdhistoryrequest.py +59 -0
- finance_dev/models/predmdhistoryresponse.py +53 -0
- finance_dev/models/predmdtick.py +53 -0
- finance_dev/models/predordersapperror.py +78 -0
- finance_dev/models/predordersorder.py +267 -0
- finance_dev/models/predordersorderdetail.py +26 -0
- finance_dev/models/predordersorderpage.py +45 -0
- finance_dev/models/predordersorderreport.py +88 -0
- finance_dev/models/predordersplaceorderrequest.py +159 -0
- finance_dev/models/previeworderop.py +40 -0
- finance_dev/models/previeworderresponse.py +127 -0
- finance_dev/models/pricetargetresponse.py +89 -0
- finance_dev/models/quotedata.py +49 -0
- finance_dev/models/quotesresponse.py +62 -0
- finance_dev/models/quotevalues.py +114 -0
- finance_dev/models/rating.py +193 -0
- finance_dev/models/ratingsaccuracy.py +242 -0
- finance_dev/models/replaceconfigwatchlistsop.py +40 -0
- finance_dev/models/replaceorderop.py +40 -0
- finance_dev/models/replaceordrequest_union.py +367 -0
- finance_dev/models/replaceordresponse.py +144 -0
- finance_dev/models/reportedbsitem.py +53 -0
- finance_dev/models/reportedfiling.py +110 -0
- finance_dev/models/reportedreport.py +39 -0
- finance_dev/models/reportingfrequency.py +16 -0
- finance_dev/models/revenuebreakdownaxis.py +52 -0
- finance_dev/models/revenuebreakdowndatapoint.py +58 -0
- finance_dev/models/revenuebreakdownentry.py +51 -0
- finance_dev/models/revenuebreakdowninner.py +75 -0
- finance_dev/models/revenueestimatesresponse.py +176 -0
- finance_dev/models/screenclause.py +70 -0
- finance_dev/models/screengroup.py +75 -0
- finance_dev/models/searchetbsymbolsop.py +40 -0
- finance_dev/models/searchindicesop.py +55 -0
- finance_dev/models/searchlogosop.py +158 -0
- finance_dev/models/searchmarketsymbolsop.py +80 -0
- finance_dev/models/searchop.py +232 -0
- finance_dev/models/searchsymbolitem.py +76 -0
- finance_dev/models/searchsymbolresponseitem.py +71 -0
- finance_dev/models/searchsymbolsop.py +77 -0
- finance_dev/models/secfiling.py +91 -0
- finance_dev/models/secfilingsresponse.py +39 -0
- finance_dev/models/sectoravgchange.py +50 -0
- finance_dev/models/security.py +59 -0
- finance_dev/models/securitytypeenum.py +16 -0
- finance_dev/models/shortinterestresponse.py +126 -0
- finance_dev/models/shortvolumeresponse.py +198 -0
- finance_dev/models/socialsentimentresponse.py +124 -0
- finance_dev/models/sortorderenum.py +13 -0
- finance_dev/models/spinoffsresponse.py +136 -0
- finance_dev/models/stockaggregatesresponse.py +92 -0
- finance_dev/models/stocklastquoteresponse.py +175 -0
- finance_dev/models/stocklasttraderesponse.py +169 -0
- finance_dev/models/stockohlcbar.py +85 -0
- finance_dev/models/stockopencloseresponse.py +109 -0
- finance_dev/models/stockpreviousdayresponse.py +86 -0
- finance_dev/models/stockquotesresponse.py +170 -0
- finance_dev/models/stockscreenerop.py +144 -0
- finance_dev/models/stockscreenerrequest.py +131 -0
- finance_dev/models/stockscreenerresponse.py +197 -0
- finance_dev/models/stocksnapshotsresponse.py +454 -0
- finance_dev/models/stocksplitsresponse.py +144 -0
- finance_dev/models/stocktradesresponse.py +164 -0
- finance_dev/models/supplychainresponse.py +158 -0
- finance_dev/models/supportresistancelevelsresponse.py +68 -0
- finance_dev/models/symboldetails.py +119 -0
- finance_dev/models/symbolerror.py +43 -0
- finance_dev/models/symbolinfo.py +183 -0
- finance_dev/models/synclogosop.py +146 -0
- finance_dev/models/technicalindicatorresponse.py +43 -0
- finance_dev/models/technicalindicatortype.py +21 -0
- finance_dev/models/technicalpatternsresponse.py +249 -0
- finance_dev/models/tenderofferidentifiertype.py +26 -0
- finance_dev/models/tenderoffersresponse.py +548 -0
- finance_dev/models/tickchartbars.py +104 -0
- finance_dev/models/tickresolution.py +20 -0
- finance_dev/models/timeinforceenum.py +26 -0
- finance_dev/models/tradeorderlegv2.py +164 -0
- finance_dev/models/tradeorderv2.py +434 -0
- finance_dev/models/transcriptresponse.py +162 -0
- finance_dev/models/transcriptsresponse.py +89 -0
- finance_dev/models/treasuryyieldsresponse.py +160 -0
- finance_dev/models/unifiedsnapshotresponse.py +308 -0
- finance_dev/models/updateconfigwatchlistbyidop.py +65 -0
- finance_dev/models/updatewatchlistrequest_union.py +134 -0
- finance_dev/models/upgradesdowngradesresponse.py +98 -0
- finance_dev/models/usaspendingresponse.py +222 -0
- finance_dev/models/usptopatentsresponse.py +134 -0
- finance_dev/models/visaapplicationsresponse.py +228 -0
- finance_dev/models/watchlist.py +68 -0
- finance_dev/models/watchlistrequest.py +43 -0
- finance_dev/models/watchlistresponse.py +44 -0
- finance_dev/news_sdk.py +829 -0
- finance_dev/options.py +3443 -0
- finance_dev/orders.py +1907 -0
- finance_dev/ownership.py +1386 -0
- finance_dev/predictions.py +29 -0
- finance_dev/predictions_orders.py +852 -0
- finance_dev/py.typed +1 -0
- finance_dev/sdk.py +347 -0
- finance_dev/sdkconfiguration.py +50 -0
- finance_dev/signals.py +338 -0
- finance_dev/stocks.py +3131 -0
- finance_dev/technicalanalysis_sdk.py +1137 -0
- finance_dev/types/__init__.py +25 -0
- finance_dev/types/base64fileinput.py +44 -0
- finance_dev/types/basemodel.py +78 -0
- finance_dev/utils/__init__.py +181 -0
- finance_dev/utils/annotations.py +80 -0
- finance_dev/utils/datetimes.py +42 -0
- finance_dev/utils/dynamic_imports.py +55 -0
- finance_dev/utils/enums.py +135 -0
- finance_dev/utils/eventstreaming.py +327 -0
- finance_dev/utils/forms.py +240 -0
- finance_dev/utils/headers.py +137 -0
- finance_dev/utils/logger.py +23 -0
- finance_dev/utils/metadata.py +120 -0
- finance_dev/utils/queryparams.py +218 -0
- finance_dev/utils/requestbodies.py +68 -0
- finance_dev/utils/retries.py +373 -0
- finance_dev/utils/security.py +199 -0
- finance_dev/utils/serializers.py +327 -0
- finance_dev/utils/unmarshal_json_response.py +39 -0
- finance_dev/utils/url.py +155 -0
- finance_dev/utils/values.py +143 -0
- finance_dev/watchlist_sdk.py +1583 -0
- finance_dev/websocket/__init__.py +57 -0
- finance_dev/websocket/account.py +5 -0
- finance_dev/websocket/auth.py +124 -0
- finance_dev/websocket/chart.py +5 -0
- finance_dev/websocket/codec.py +66 -0
- finance_dev/websocket/config.py +52 -0
- finance_dev/websocket/errors.py +54 -0
- finance_dev/websocket/market.py +5 -0
- finance_dev/websocket/models/__init__.py +52 -0
- finance_dev/websocket/models/account.py +60 -0
- finance_dev/websocket/models/chart.py +44 -0
- finance_dev/websocket/models/common.py +85 -0
- finance_dev/websocket/models/market.py +113 -0
- finance_dev/websocket/resources.py +327 -0
- finance_dev/websocket/sdk.py +13 -0
- finance_dev/websocket/transport/async_connection.py +493 -0
- finance_dev/websocket/transport/base.py +78 -0
- finance_dev/websocket/transport/sync_connection.py +470 -0
- finance_dev-0.9.0.dist-info/METADATA +913 -0
- finance_dev-0.9.0.dist-info/RECORD +486 -0
- finance_dev-0.9.0.dist-info/WHEEL +5 -0
- finance_dev-0.9.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
"""Asyncio websocket connection with a single reader task."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import inspect
|
|
5
|
+
import random
|
|
6
|
+
import uuid
|
|
7
|
+
from typing import Any
|
|
8
|
+
from ..auth import AsyncTokenProvider, TokenProvider, StaticTokenProvider
|
|
9
|
+
from ..codec import decode_frame, encode_message
|
|
10
|
+
from ..config import WebSocketConfig
|
|
11
|
+
from ..errors import (
|
|
12
|
+
WebSocketAuthenticationError,
|
|
13
|
+
WebSocketBackpressureError,
|
|
14
|
+
WebSocketConnectionError,
|
|
15
|
+
WebSocketConnectionStateError,
|
|
16
|
+
WebSocketProtocolError,
|
|
17
|
+
WebSocketRequestError,
|
|
18
|
+
)
|
|
19
|
+
from .base import apply_unsubscribe, subscription_key
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class _SessionExpired(ConnectionError):
|
|
23
|
+
"""Internal signal to reconnect after server-side token expiry."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class AsyncConnection:
|
|
27
|
+
def __init__(
|
|
28
|
+
self, url: str, token_provider: Any, config: WebSocketConfig | None = None
|
|
29
|
+
):
|
|
30
|
+
self.url, self.provider, self.config = (
|
|
31
|
+
url,
|
|
32
|
+
token_provider,
|
|
33
|
+
config or WebSocketConfig(),
|
|
34
|
+
)
|
|
35
|
+
self._state = "DISCONNECTED"
|
|
36
|
+
self._socket: Any = None
|
|
37
|
+
self._reader: asyncio.Task | None = None
|
|
38
|
+
self.resource: Any = None
|
|
39
|
+
self._pending: dict[str, asyncio.Future] = {}
|
|
40
|
+
self._events: asyncio.Queue[Any] = asyncio.Queue(
|
|
41
|
+
self.config.incoming_queue_size
|
|
42
|
+
)
|
|
43
|
+
self._closed = False
|
|
44
|
+
self._auth_id: str | None = None
|
|
45
|
+
self._active: dict[str, dict[str, Any]] = {}
|
|
46
|
+
self._keepalive: asyncio.Task | None = None
|
|
47
|
+
self._terminal: Exception | None = None
|
|
48
|
+
self._terminal_reported = False
|
|
49
|
+
|
|
50
|
+
async def connect(self):
|
|
51
|
+
try:
|
|
52
|
+
from websockets.asyncio.client import connect
|
|
53
|
+
|
|
54
|
+
self._state = "CONNECTING"
|
|
55
|
+
self._socket = await asyncio.wait_for(
|
|
56
|
+
connect(
|
|
57
|
+
self.url,
|
|
58
|
+
open_timeout=self.config.connect_timeout,
|
|
59
|
+
close_timeout=self.config.close_timeout,
|
|
60
|
+
max_size=self.config.max_message_size,
|
|
61
|
+
ping_interval=self.config.ping_interval,
|
|
62
|
+
ping_timeout=self.config.pong_timeout,
|
|
63
|
+
),
|
|
64
|
+
self.config.connect_timeout,
|
|
65
|
+
)
|
|
66
|
+
self._state = "AUTHENTICATING"
|
|
67
|
+
self._reader = asyncio.create_task(self._read_loop())
|
|
68
|
+
self._auth_id = self._id("auth")
|
|
69
|
+
auth_future = self._pending_future(self._auth_id)
|
|
70
|
+
await self._send(
|
|
71
|
+
{
|
|
72
|
+
"type": "request",
|
|
73
|
+
"id": self._auth_id,
|
|
74
|
+
"payload": {
|
|
75
|
+
"method": "POST",
|
|
76
|
+
"path": "/auth",
|
|
77
|
+
"body": {"token": await self._get_token()},
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
)
|
|
81
|
+
result = await asyncio.wait_for(auth_future, self.config.auth_timeout)
|
|
82
|
+
if result.get("payload", {}).get("action") != "authSuccess":
|
|
83
|
+
raise WebSocketAuthenticationError(
|
|
84
|
+
"WebSocket authentication was rejected"
|
|
85
|
+
)
|
|
86
|
+
self._state = "READY"
|
|
87
|
+
self._keepalive = asyncio.create_task(self._keepalive_loop())
|
|
88
|
+
return self
|
|
89
|
+
except asyncio.TimeoutError as exc:
|
|
90
|
+
await self.close()
|
|
91
|
+
raise WebSocketAuthenticationError(
|
|
92
|
+
"WebSocket authentication timed out"
|
|
93
|
+
) from exc
|
|
94
|
+
except (WebSocketAuthenticationError, WebSocketConnectionError):
|
|
95
|
+
await self.close()
|
|
96
|
+
raise
|
|
97
|
+
except WebSocketRequestError as exc:
|
|
98
|
+
await self.close()
|
|
99
|
+
raise WebSocketAuthenticationError(
|
|
100
|
+
"WebSocket authentication was rejected"
|
|
101
|
+
) from exc
|
|
102
|
+
except Exception as exc:
|
|
103
|
+
await self.close()
|
|
104
|
+
raise WebSocketConnectionError(
|
|
105
|
+
"Unable to connect or authenticate websocket"
|
|
106
|
+
) from exc
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def state(self):
|
|
110
|
+
"""Current read-only connection state."""
|
|
111
|
+
return self._state
|
|
112
|
+
|
|
113
|
+
def _id(self, prefix):
|
|
114
|
+
return f"{prefix}-{uuid.uuid4()}"
|
|
115
|
+
|
|
116
|
+
async def _get_token(self):
|
|
117
|
+
value = self.provider.get_token()
|
|
118
|
+
return await value if inspect.isawaitable(value) else value
|
|
119
|
+
|
|
120
|
+
def _pending_future(self, request_id):
|
|
121
|
+
future = asyncio.get_running_loop().create_future()
|
|
122
|
+
self._pending[request_id] = future
|
|
123
|
+
return future
|
|
124
|
+
|
|
125
|
+
async def _send(self, value):
|
|
126
|
+
await self._socket.send(encode_message(value))
|
|
127
|
+
|
|
128
|
+
async def send_message(self, message):
|
|
129
|
+
self._require_ready()
|
|
130
|
+
key = subscription_key(message)
|
|
131
|
+
if message.get("type") == "subscribe" and key in self._active:
|
|
132
|
+
return message.get("id")
|
|
133
|
+
await self._send(message)
|
|
134
|
+
if message.get("type") == "subscribe":
|
|
135
|
+
self._active[key] = dict(message)
|
|
136
|
+
elif message.get("type") == "unsubscribe":
|
|
137
|
+
apply_unsubscribe(self._active, message)
|
|
138
|
+
return message.get("id")
|
|
139
|
+
|
|
140
|
+
async def exchange(self, message, *, timeout=None):
|
|
141
|
+
self._require_ready()
|
|
142
|
+
ident = message.get("id") or self._id("request")
|
|
143
|
+
message["id"] = ident
|
|
144
|
+
future = self._pending_future(ident)
|
|
145
|
+
await self._send(message)
|
|
146
|
+
try:
|
|
147
|
+
return await asyncio.wait_for(
|
|
148
|
+
future, timeout or self.config.request_timeout
|
|
149
|
+
)
|
|
150
|
+
finally:
|
|
151
|
+
self._pending.pop(ident, None)
|
|
152
|
+
|
|
153
|
+
async def _read_loop(self):
|
|
154
|
+
try:
|
|
155
|
+
async for frame in self._socket:
|
|
156
|
+
for message in decode_frame(frame):
|
|
157
|
+
if hasattr(message, "original"):
|
|
158
|
+
await self._put(message)
|
|
159
|
+
continue
|
|
160
|
+
wire_message = message
|
|
161
|
+
message = message.model_dump(by_alias=True, exclude_none=True)
|
|
162
|
+
payload = message.get("payload", {})
|
|
163
|
+
ident = message.get("id") or payload.get("id")
|
|
164
|
+
action = payload.get("action")
|
|
165
|
+
if action == "authExpired":
|
|
166
|
+
raise _SessionExpired("WebSocket session expired")
|
|
167
|
+
if action in {"authRequired", "authTimeout"}:
|
|
168
|
+
raise WebSocketAuthenticationError(
|
|
169
|
+
str(payload.get("error", action))
|
|
170
|
+
)
|
|
171
|
+
if action == "refreshAuth":
|
|
172
|
+
asyncio.create_task(self._refresh_auth())
|
|
173
|
+
continue
|
|
174
|
+
if ident in self._pending and (
|
|
175
|
+
message.get("type") in {"response", "subscribed", "pong"}
|
|
176
|
+
or action in {"authSuccess", "error"}
|
|
177
|
+
):
|
|
178
|
+
future = self._pending.pop(ident)
|
|
179
|
+
if payload.get("error") or payload.get("status", 200) >= 400:
|
|
180
|
+
if ident == self._auth_id:
|
|
181
|
+
future.set_exception(
|
|
182
|
+
WebSocketAuthenticationError(
|
|
183
|
+
"WebSocket authentication was rejected"
|
|
184
|
+
)
|
|
185
|
+
)
|
|
186
|
+
else:
|
|
187
|
+
future.set_exception(
|
|
188
|
+
WebSocketRequestError(
|
|
189
|
+
str(
|
|
190
|
+
payload.get(
|
|
191
|
+
"message",
|
|
192
|
+
payload.get("error", "request failed"),
|
|
193
|
+
)
|
|
194
|
+
),
|
|
195
|
+
status=payload.get("status"),
|
|
196
|
+
code=payload.get("code"),
|
|
197
|
+
request_id=ident,
|
|
198
|
+
)
|
|
199
|
+
)
|
|
200
|
+
elif action == "error":
|
|
201
|
+
future.set_exception(
|
|
202
|
+
WebSocketRequestError(
|
|
203
|
+
str(payload.get("error", "request failed")),
|
|
204
|
+
status=payload.get("status"),
|
|
205
|
+
code=payload.get("code"),
|
|
206
|
+
request_id=ident,
|
|
207
|
+
)
|
|
208
|
+
)
|
|
209
|
+
elif not future.done():
|
|
210
|
+
future.set_result(message)
|
|
211
|
+
elif action == "authSuccess" and self._auth_id in self._pending:
|
|
212
|
+
self._pending.pop(self._auth_id).set_result(message)
|
|
213
|
+
else:
|
|
214
|
+
await self._put(wire_message)
|
|
215
|
+
except asyncio.CancelledError:
|
|
216
|
+
raise
|
|
217
|
+
except Exception as exc:
|
|
218
|
+
if self._closed:
|
|
219
|
+
return
|
|
220
|
+
transient = isinstance(
|
|
221
|
+
exc, (OSError, ConnectionError, TimeoutError)
|
|
222
|
+
) or exc.__class__.__name__.startswith("ConnectionClosed")
|
|
223
|
+
if (
|
|
224
|
+
self.config.reconnect
|
|
225
|
+
and transient
|
|
226
|
+
and not isinstance(
|
|
227
|
+
exc,
|
|
228
|
+
(
|
|
229
|
+
WebSocketProtocolError,
|
|
230
|
+
WebSocketAuthenticationError,
|
|
231
|
+
WebSocketRequestError,
|
|
232
|
+
WebSocketBackpressureError,
|
|
233
|
+
),
|
|
234
|
+
)
|
|
235
|
+
):
|
|
236
|
+
try:
|
|
237
|
+
await self._recover()
|
|
238
|
+
except Exception as reconnect_error:
|
|
239
|
+
self._state = "FAILED"
|
|
240
|
+
self._terminal = reconnect_error
|
|
241
|
+
await self._put_error(reconnect_error)
|
|
242
|
+
else:
|
|
243
|
+
self._state = "FAILED"
|
|
244
|
+
self._terminal = exc
|
|
245
|
+
await self._put_error(exc)
|
|
246
|
+
|
|
247
|
+
async def _refresh_auth(self):
|
|
248
|
+
try:
|
|
249
|
+
token = await self._get_token()
|
|
250
|
+
ident = self._id("auth")
|
|
251
|
+
future = self._pending_future(ident)
|
|
252
|
+
await self._send(
|
|
253
|
+
{
|
|
254
|
+
"type": "request",
|
|
255
|
+
"id": ident,
|
|
256
|
+
"payload": {
|
|
257
|
+
"method": "POST",
|
|
258
|
+
"path": "/auth",
|
|
259
|
+
"body": {"token": token},
|
|
260
|
+
},
|
|
261
|
+
}
|
|
262
|
+
)
|
|
263
|
+
result = await asyncio.wait_for(future, self.config.auth_timeout)
|
|
264
|
+
if result.get("payload", {}).get("action") != "authSuccess":
|
|
265
|
+
raise WebSocketAuthenticationError(
|
|
266
|
+
"WebSocket token refresh was rejected"
|
|
267
|
+
)
|
|
268
|
+
except Exception as exc:
|
|
269
|
+
await self._put_error(exc)
|
|
270
|
+
await self.close()
|
|
271
|
+
|
|
272
|
+
async def _recover(self):
|
|
273
|
+
self._state = "RECONNECTING"
|
|
274
|
+
if self._keepalive:
|
|
275
|
+
self._keepalive.cancel()
|
|
276
|
+
if self._socket:
|
|
277
|
+
try:
|
|
278
|
+
await self._socket.close()
|
|
279
|
+
except Exception:
|
|
280
|
+
pass
|
|
281
|
+
attempt = 0
|
|
282
|
+
while not self._closed:
|
|
283
|
+
delay = min(
|
|
284
|
+
self.config.reconnect_max_delay,
|
|
285
|
+
self.config.reconnect_initial_delay * (2**attempt),
|
|
286
|
+
)
|
|
287
|
+
await asyncio.sleep(
|
|
288
|
+
delay
|
|
289
|
+
* random.uniform(
|
|
290
|
+
1 - self.config.reconnect_jitter, 1 + self.config.reconnect_jitter
|
|
291
|
+
)
|
|
292
|
+
)
|
|
293
|
+
try:
|
|
294
|
+
from websockets.asyncio.client import connect
|
|
295
|
+
|
|
296
|
+
self._socket = await asyncio.wait_for(
|
|
297
|
+
connect(
|
|
298
|
+
self.url,
|
|
299
|
+
open_timeout=self.config.connect_timeout,
|
|
300
|
+
close_timeout=self.config.close_timeout,
|
|
301
|
+
max_size=self.config.max_message_size,
|
|
302
|
+
ping_interval=self.config.ping_interval,
|
|
303
|
+
ping_timeout=self.config.pong_timeout,
|
|
304
|
+
),
|
|
305
|
+
self.config.connect_timeout,
|
|
306
|
+
)
|
|
307
|
+
self._state = "AUTHENTICATING"
|
|
308
|
+
self._auth_id = self._id("auth")
|
|
309
|
+
future = self._pending_future(self._auth_id)
|
|
310
|
+
self._reader = asyncio.create_task(self._read_loop())
|
|
311
|
+
token = await self._get_token()
|
|
312
|
+
await self._send(
|
|
313
|
+
{
|
|
314
|
+
"type": "request",
|
|
315
|
+
"id": self._auth_id,
|
|
316
|
+
"payload": {
|
|
317
|
+
"method": "POST",
|
|
318
|
+
"path": "/auth",
|
|
319
|
+
"body": {"token": token},
|
|
320
|
+
},
|
|
321
|
+
}
|
|
322
|
+
)
|
|
323
|
+
result = await asyncio.wait_for(future, self.config.auth_timeout)
|
|
324
|
+
if result.get("payload", {}).get("action") != "authSuccess":
|
|
325
|
+
raise WebSocketAuthenticationError(
|
|
326
|
+
"WebSocket authentication was rejected"
|
|
327
|
+
)
|
|
328
|
+
self._state = "READY"
|
|
329
|
+
if self.config.replay_subscriptions:
|
|
330
|
+
for message in self._active.values():
|
|
331
|
+
await self._send(message)
|
|
332
|
+
self._keepalive = asyncio.create_task(self._keepalive_loop())
|
|
333
|
+
return
|
|
334
|
+
except WebSocketAuthenticationError:
|
|
335
|
+
self._state = "FAILED"
|
|
336
|
+
raise
|
|
337
|
+
except WebSocketRequestError as exc:
|
|
338
|
+
self._state = "FAILED"
|
|
339
|
+
raise WebSocketAuthenticationError(
|
|
340
|
+
"WebSocket authentication was rejected"
|
|
341
|
+
) from exc
|
|
342
|
+
except Exception as exc:
|
|
343
|
+
attempt += 1
|
|
344
|
+
if attempt >= 8:
|
|
345
|
+
self._state = "FAILED"
|
|
346
|
+
raise WebSocketConnectionError(
|
|
347
|
+
"WebSocket reconnect attempts exhausted"
|
|
348
|
+
) from exc
|
|
349
|
+
|
|
350
|
+
async def _keepalive_loop(self):
|
|
351
|
+
try:
|
|
352
|
+
while not self._closed and self._state == "READY":
|
|
353
|
+
await asyncio.sleep(self.config.ping_interval)
|
|
354
|
+
ident = self._id("ping")
|
|
355
|
+
future = self._pending_future(ident)
|
|
356
|
+
await self._send({"type": "ping", "id": ident})
|
|
357
|
+
await asyncio.wait_for(future, self.config.pong_timeout)
|
|
358
|
+
except asyncio.CancelledError:
|
|
359
|
+
pass
|
|
360
|
+
except Exception:
|
|
361
|
+
if self._socket and not self._closed:
|
|
362
|
+
try:
|
|
363
|
+
await self._socket.close()
|
|
364
|
+
except Exception:
|
|
365
|
+
pass
|
|
366
|
+
|
|
367
|
+
async def _put(self, event):
|
|
368
|
+
try:
|
|
369
|
+
self._events.put_nowait(event)
|
|
370
|
+
except asyncio.QueueFull:
|
|
371
|
+
self._terminal = WebSocketBackpressureError(
|
|
372
|
+
"Incoming websocket queue is full; drain events faster or increase incoming_queue_size"
|
|
373
|
+
)
|
|
374
|
+
self._closed = True
|
|
375
|
+
self._state = "FAILED"
|
|
376
|
+
try:
|
|
377
|
+
await self._socket.close()
|
|
378
|
+
finally:
|
|
379
|
+
await self._put_error(self._terminal)
|
|
380
|
+
|
|
381
|
+
async def _put_error(self, error):
|
|
382
|
+
self._terminal = error
|
|
383
|
+
self._closed = True
|
|
384
|
+
try:
|
|
385
|
+
self._events.put_nowait(error)
|
|
386
|
+
except asyncio.QueueFull:
|
|
387
|
+
pass
|
|
388
|
+
|
|
389
|
+
async def request(self, method, path, *, query=None, body=None, timeout=None):
|
|
390
|
+
payload = {"method": method, "path": path}
|
|
391
|
+
if query is not None:
|
|
392
|
+
payload["query"] = query
|
|
393
|
+
if body is not None:
|
|
394
|
+
payload["body"] = body
|
|
395
|
+
return await self.exchange(
|
|
396
|
+
{"type": "request", "id": self._id("request"), "payload": payload},
|
|
397
|
+
timeout=timeout,
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
def _require_ready(self):
|
|
401
|
+
if self.state != "READY":
|
|
402
|
+
raise WebSocketConnectionStateError(
|
|
403
|
+
"WebSocket must be authenticated and READY before this operation"
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
async def subscribe(self, payload, *, data_type="quotes", symbol_type=None):
|
|
407
|
+
resource: Any = getattr(self, "resource", None)
|
|
408
|
+
if getattr(resource, "protocol", "") == "chart":
|
|
409
|
+
return await resource.subscribe_async(
|
|
410
|
+
self, payload, data_type=data_type, symbol_type=symbol_type
|
|
411
|
+
)
|
|
412
|
+
if getattr(resource, "protocol", "") == "market":
|
|
413
|
+
return await resource.subscribe_async(self, payload)
|
|
414
|
+
ident = self._id("subscribe")
|
|
415
|
+
await self.send_message({"type": "subscribe", "id": ident, "payload": payload})
|
|
416
|
+
return ident
|
|
417
|
+
|
|
418
|
+
async def unsubscribe(self, payload):
|
|
419
|
+
resource: Any = getattr(self, "resource", None)
|
|
420
|
+
if getattr(resource, "protocol", "") == "chart":
|
|
421
|
+
return await resource.unsubscribe_async(self, payload)
|
|
422
|
+
if getattr(resource, "protocol", "") == "market":
|
|
423
|
+
return await resource.unsubscribe_async(self, payload)
|
|
424
|
+
ident = self._id("unsubscribe")
|
|
425
|
+
await self.send_message(
|
|
426
|
+
{"type": "unsubscribe", "id": ident, "payload": payload}
|
|
427
|
+
)
|
|
428
|
+
return ident
|
|
429
|
+
|
|
430
|
+
async def get_event(self):
|
|
431
|
+
item = await self._events.get()
|
|
432
|
+
if isinstance(item, Exception):
|
|
433
|
+
self._terminal_reported = True
|
|
434
|
+
raise item
|
|
435
|
+
return item
|
|
436
|
+
|
|
437
|
+
def __aiter__(self):
|
|
438
|
+
return self
|
|
439
|
+
|
|
440
|
+
def __getattr__(self, name):
|
|
441
|
+
resource = self.__dict__.get("resource")
|
|
442
|
+
method: Any = getattr(resource, name, None) if resource is not None else None
|
|
443
|
+
if method is not None and callable(method):
|
|
444
|
+
|
|
445
|
+
async def delegated(*args, **kwargs):
|
|
446
|
+
result: Any = method(self, *args, **kwargs)
|
|
447
|
+
return await result if inspect.isawaitable(result) else result
|
|
448
|
+
|
|
449
|
+
return delegated
|
|
450
|
+
raise AttributeError(name)
|
|
451
|
+
|
|
452
|
+
async def __anext__(self):
|
|
453
|
+
if self._closed and self._events.empty():
|
|
454
|
+
if self._terminal and not self._terminal_reported:
|
|
455
|
+
self._terminal_reported = True
|
|
456
|
+
raise self._terminal
|
|
457
|
+
raise StopAsyncIteration
|
|
458
|
+
return await self.get_event()
|
|
459
|
+
|
|
460
|
+
async def close(self):
|
|
461
|
+
if self._closed and self._state == "CLOSED":
|
|
462
|
+
return
|
|
463
|
+
self._closed = True
|
|
464
|
+
self._state = "CLOSING"
|
|
465
|
+
for future in self._pending.values():
|
|
466
|
+
if not future.done():
|
|
467
|
+
future.cancel()
|
|
468
|
+
self._pending.clear()
|
|
469
|
+
if self._keepalive:
|
|
470
|
+
self._keepalive.cancel()
|
|
471
|
+
if self._keepalive is not asyncio.current_task():
|
|
472
|
+
try:
|
|
473
|
+
await self._keepalive
|
|
474
|
+
except BaseException:
|
|
475
|
+
pass
|
|
476
|
+
if self._socket:
|
|
477
|
+
try:
|
|
478
|
+
await asyncio.wait_for(self._socket.close(), self.config.close_timeout)
|
|
479
|
+
except Exception:
|
|
480
|
+
pass
|
|
481
|
+
if self._reader and self._reader is not asyncio.current_task():
|
|
482
|
+
self._reader.cancel()
|
|
483
|
+
try:
|
|
484
|
+
await self._reader
|
|
485
|
+
except BaseException:
|
|
486
|
+
pass
|
|
487
|
+
self._state = "CLOSED"
|
|
488
|
+
|
|
489
|
+
async def __aenter__(self):
|
|
490
|
+
return await self.connect()
|
|
491
|
+
|
|
492
|
+
async def __aexit__(self, *_):
|
|
493
|
+
await self.close()
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Protocol-independent subscription replay helpers."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def subscription_key(message: dict[str, Any]) -> str:
|
|
8
|
+
return json.dumps(
|
|
9
|
+
{"type": message.get("type"), "payload": message.get("payload")},
|
|
10
|
+
sort_keys=True,
|
|
11
|
+
separators=(",", ":"),
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def apply_unsubscribe(
|
|
16
|
+
active: dict[str, dict[str, Any]], message: dict[str, Any]
|
|
17
|
+
) -> None:
|
|
18
|
+
"""Update canonical active subscriptions for full and field-level removals."""
|
|
19
|
+
payload = message.get("payload")
|
|
20
|
+
requested = payload if isinstance(payload, list) else [payload]
|
|
21
|
+
for removal in requested:
|
|
22
|
+
if isinstance(removal, str):
|
|
23
|
+
removal = {"symbol": removal}
|
|
24
|
+
if not isinstance(removal, dict):
|
|
25
|
+
continue
|
|
26
|
+
symbol = removal.get("symbol")
|
|
27
|
+
if not symbol:
|
|
28
|
+
continue
|
|
29
|
+
full = set(removal) <= {"symbol", "type"}
|
|
30
|
+
for key, subscription in list(active.items()):
|
|
31
|
+
body = subscription.get("payload")
|
|
32
|
+
items = body if isinstance(body, list) else [body]
|
|
33
|
+
kept = []
|
|
34
|
+
for original in items:
|
|
35
|
+
if not isinstance(original, dict) or original.get("symbol") != symbol:
|
|
36
|
+
kept.append(original)
|
|
37
|
+
continue
|
|
38
|
+
current = dict(original)
|
|
39
|
+
if full:
|
|
40
|
+
continue
|
|
41
|
+
for category in ("quote", "trade", "level2", "timeAndSales", "greeks"):
|
|
42
|
+
if removal.get(category):
|
|
43
|
+
if category == "quote":
|
|
44
|
+
current.pop("quoteFields", None)
|
|
45
|
+
elif category == "trade":
|
|
46
|
+
current.pop("tradeFields", None)
|
|
47
|
+
elif category == "greeks":
|
|
48
|
+
current.pop("greeksFields", None)
|
|
49
|
+
else:
|
|
50
|
+
current.pop(category, None)
|
|
51
|
+
for field in ("quoteFields", "tradeFields", "greeksFields"):
|
|
52
|
+
removed = removal.get(field)
|
|
53
|
+
existing = current.get(field)
|
|
54
|
+
if removed and isinstance(existing, list):
|
|
55
|
+
remaining = [
|
|
56
|
+
value for value in existing if value not in removed
|
|
57
|
+
]
|
|
58
|
+
if remaining:
|
|
59
|
+
current[field] = remaining
|
|
60
|
+
else:
|
|
61
|
+
current.pop(field, None)
|
|
62
|
+
# Chart messages carry a single data mode in `type` and have no
|
|
63
|
+
# other fields to preserve after the symbol is removed.
|
|
64
|
+
if len(current) <= 2 and "symbol" in current and "type" in current:
|
|
65
|
+
continue
|
|
66
|
+
if len(current) == 1 and "symbol" in current:
|
|
67
|
+
continue
|
|
68
|
+
kept.append(current)
|
|
69
|
+
if not kept:
|
|
70
|
+
active.pop(key, None)
|
|
71
|
+
elif isinstance(body, list):
|
|
72
|
+
subscription["payload"] = kept
|
|
73
|
+
active.pop(key, None)
|
|
74
|
+
active[subscription_key(subscription)] = subscription
|
|
75
|
+
else:
|
|
76
|
+
subscription["payload"] = kept[0]
|
|
77
|
+
active.pop(key, None)
|
|
78
|
+
active[subscription_key(subscription)] = subscription
|