diff --git a/.gitignore b/.gitignore index c61c65c2..dc3162b6 100644 --- a/.gitignore +++ b/.gitignore @@ -123,6 +123,9 @@ docs/_build/ # PyBuilder target/ +# Ruff cache +.ruff_cache/ + # Jupyter Notebook .ipynb_checkpoints diff --git a/dev_requirements.txt b/dev_requirements.txt deleted file mode 100644 index 718d8a71..00000000 --- a/dev_requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -pytest>=4.6.11 -responses>=0.12.1 -pytest-cov>=2.10.1 -flake8>=3.8.4, <= 4.0.1 -mock>=3.0.5 -urllib3<2.0 \ No newline at end of file diff --git a/examples/flask_app.py b/examples/flask_app.py index 88d19de4..c7df468d 100644 --- a/examples/flask_app.py +++ b/examples/flask_app.py @@ -30,7 +30,8 @@ HOST = "127.0.0.1" -def serializer(obj): return isinstance(obj, (date, datetime, Decimal)) and str(obj) # noqa +def serializer(obj): + return isinstance(obj, (date, datetime, Decimal)) and str(obj) # noqa # Kite Connect App settings. Go to https://developers.kite.trade/apps/ @@ -42,10 +43,14 @@ def serializer(obj): return isinstance(obj, (date, datetime, Decimal)) and str(o redirect_url = "http://{host}:{port}/login".format(host=HOST, port=PORT) # Login url -login_url = "https://kite.zerodha.com/connect/login?api_key={api_key}".format(api_key=kite_api_key) +login_url = "https://kite.zerodha.com/connect/login?api_key={api_key}".format( + api_key=kite_api_key +) # Kite connect console url -console_url = "https://developers.kite.trade/apps/{api_key}".format(api_key=kite_api_key) +console_url = "https://developers.kite.trade/apps/{api_key}".format( + api_key=kite_api_key +) # App app = Flask(__name__) @@ -68,8 +73,7 @@ def serializer(obj): return isinstance(obj, (date, datetime, Decimal)) and str(o def get_kite_client(): - """Returns a kite client object - """ + """Returns a kite client object""" kite = KiteConnect(api_key=kite_api_key) if "access_token" in session: kite.set_access_token(session["access_token"]) @@ -82,7 +86,7 @@ def index(): api_key=kite_api_key, redirect_url=redirect_url, console_url=console_url, - login_url=login_url + login_url=login_url, ) @@ -103,12 +107,7 @@ def login(): return login_template.format( access_token=data["access_token"], - user_data=json.dumps( - data, - indent=4, - sort_keys=True, - default=serializer - ) + user_data=json.dumps(data, indent=4, sort_keys=True, default=serializer), ) diff --git a/examples/gtt_order.py b/examples/gtt_order.py index 911b61cb..7e50bbde 100644 --- a/examples/gtt_order.py +++ b/examples/gtt_order.py @@ -11,46 +11,65 @@ # Once you have the request_token, obtain the access_token # as follows. -data = kite.generate_session("request_token_here", secret="your_secret") +data = kite.generate_session("request_token_here", api_secret="your_secret") kite.set_access_token(data["access_token"]) # Place single-leg gtt order - https://kite.trade/docs/connect/v3/gtt/#single try: - order_single = [{ - "exchange":"NSE", - "tradingsymbol": "SBIN", - "transaction_type": kite.TRANSACTION_TYPE_BUY, - "quantity": 1, - "order_type": "LIMIT", - "product": "CNC", - "price": 470, - }] - single_gtt = kite.place_gtt(trigger_type=kite.GTT_TYPE_SINGLE, tradingsymbol="SBIN", exchange="NSE", trigger_values=[470], last_price=473, orders=order_single) - logging.info("single leg gtt order trigger_id : {}".format(single_gtt['trigger_id'])) + order_single = [ + { + "exchange": "NSE", + "tradingsymbol": "SBIN", + "transaction_type": kite.TRANSACTION_TYPE_BUY, + "quantity": 1, + "order_type": "LIMIT", + "product": "CNC", + "price": 470, + } + ] + single_gtt = kite.place_gtt( + trigger_type=kite.GTT_TYPE_SINGLE, + tradingsymbol="SBIN", + exchange="NSE", + trigger_values=[470], + last_price=473, + orders=order_single, + ) + logging.info(f"single leg gtt order trigger_id : {single_gtt['trigger_id']}") except Exception as e: - logging.info("Error placing single leg gtt order: {}".format(e)) + logging.info(f"Error placing single leg gtt order: {e}") # Place two-leg(OCO) gtt order - https://kite.trade/docs/connect/v3/gtt/#two-leg try: - order_oco = [{ - "exchange":"NSE", - "tradingsymbol": "SBIN", - "transaction_type": kite.TRANSACTION_TYPE_SELL, - "quantity": 1, - "order_type": "LIMIT", - "product": "CNC", - "price": 470 - },{ - "exchange":"NSE", - "tradingsymbol": "SBIN", - "transaction_type": kite.TRANSACTION_TYPE_SELL, - "quantity": 1, - "order_type": "LIMIT", - "product": "CNC", - "price": 480 - }] - gtt_oco = kite.place_gtt(trigger_type=kite.GTT_TYPE_OCO, tradingsymbol="SBIN", exchange="NSE", trigger_values=[470,480], last_price=473, orders=order_oco) - logging.info("GTT OCO trigger_id : {}".format(gtt_oco['trigger_id'])) + order_oco = [ + { + "exchange": "NSE", + "tradingsymbol": "SBIN", + "transaction_type": kite.TRANSACTION_TYPE_SELL, + "quantity": 1, + "order_type": "LIMIT", + "product": "CNC", + "price": 470, + }, + { + "exchange": "NSE", + "tradingsymbol": "SBIN", + "transaction_type": kite.TRANSACTION_TYPE_SELL, + "quantity": 1, + "order_type": "LIMIT", + "product": "CNC", + "price": 480, + }, + ] + gtt_oco = kite.place_gtt( + trigger_type=kite.GTT_TYPE_OCO, + tradingsymbol="SBIN", + exchange="NSE", + trigger_values=[470, 480], + last_price=473, + orders=order_oco, + ) + logging.info(f"GTT OCO trigger_id : {gtt_oco['trigger_id']}") except Exception as e: - logging.info("Error placing gtt oco order: {}".format(e)) \ No newline at end of file + logging.info("Error placing gtt oco order: {}".format(e)) diff --git a/examples/order_margins.py b/examples/order_margins.py index abba2ff1..a59e8b92 100644 --- a/examples/order_margins.py +++ b/examples/order_margins.py @@ -17,78 +17,87 @@ # Fetch margin detail for order/orders try: # Fetch margin detail for single order - order_param_single = [{ - "exchange": "NSE", - "tradingsymbol": "INFY", - "transaction_type": "BUY", - "variety": "regular", - "product": "MIS", - "order_type": "MARKET", - "quantity": 2 - }] + order_param_single = [ + { + "exchange": "NSE", + "tradingsymbol": "INFY", + "transaction_type": "BUY", + "variety": "regular", + "product": "MIS", + "order_type": "MARKET", + "quantity": 2, + } + ] margin_detail = kite.order_margins(order_param_single) - logging.info("Required margin for single order: {}".format(margin_detail)) - - # Fetch margin detail for list of orders - order_param_multi = [{ - "exchange": "NSE", - "tradingsymbol": "SBIN", - "transaction_type": "BUY", - "variety": "regular", - "product": "MIS", - "order_type": "MARKET", - "quantity": 10 + logging.info("Required margin for single order: {}".format(margin_detail)) + + # Fetch margin detail for list of orders + order_param_multi = [ + { + "exchange": "NSE", + "tradingsymbol": "SBIN", + "transaction_type": "BUY", + "variety": "regular", + "product": "MIS", + "order_type": "MARKET", + "quantity": 10, }, { - "exchange": "NFO", - "tradingsymbol": "TCS20DECFUT", - "transaction_type": "BUY", - "variety": "regular", - "product": "MIS", - "order_type": "LIMIT", - "quantity": 5, - "price":2725.30 + "exchange": "NFO", + "tradingsymbol": "TCS20DECFUT", + "transaction_type": "BUY", + "variety": "regular", + "product": "MIS", + "order_type": "LIMIT", + "quantity": 5, + "price": 2725.30, }, { - "exchange": "NFO", - "tradingsymbol": "NIFTY20DECFUT", - "transaction_type": "BUY", - "variety": "bo", - "product": "MIS", - "order_type": "MARKET", - "quantity": 5 - }] + "exchange": "NFO", + "tradingsymbol": "NIFTY20DECFUT", + "transaction_type": "BUY", + "variety": "bo", + "product": "MIS", + "order_type": "MARKET", + "quantity": 5, + }, + ] margin_detail = kite.order_margins(order_param_multi) logging.info("Required margin for order_list: {}".format(margin_detail)) # Basket orders order_param_basket = [ - { - "exchange": "NFO", - "tradingsymbol": "NIFTY21JUN15400PE", - "transaction_type": "BUY", - "variety": "regular", - "product": "MIS", - "order_type": "MARKET", - "quantity": 75 - }, - { - "exchange": "NFO", - "tradingsymbol": "NIFTY21JUN14450PE", - "transaction_type": "SELL", - "variety": "regular", - "product": "MIS", - "order_type": "MARKET", - "quantity": 150 - }] + { + "exchange": "NFO", + "tradingsymbol": "NIFTY21JUN15400PE", + "transaction_type": "BUY", + "variety": "regular", + "product": "MIS", + "order_type": "MARKET", + "quantity": 75, + }, + { + "exchange": "NFO", + "tradingsymbol": "NIFTY21JUN14450PE", + "transaction_type": "SELL", + "variety": "regular", + "product": "MIS", + "order_type": "MARKET", + "quantity": 150, + }, + ] margin_amount = kite.basket_order_margins(order_param_basket) logging.info("Required margin for basket order: {}".format(margin_amount)) # Compact margin response - margin_amount_comt = kite.basket_order_margins(order_param_basket, mode='compact') - logging.info("Required margin for basket order in compact form: {}".format(margin_amount_comt)) + margin_amount_comt = kite.basket_order_margins(order_param_basket, mode="compact") + logging.info( + "Required margin for basket order in compact form: {}".format( + margin_amount_comt + ) + ) except Exception as e: logging.info("Error fetching order margin: {}".format(e)) @@ -106,7 +115,7 @@ "product": "CNC", "order_type": "MARKET", "quantity": 1, - "average_price": 560 + "average_price": 560, }, { "order_id": "2222222222", @@ -117,7 +126,7 @@ "product": "NRML", "order_type": "LIMIT", "quantity": 1, - "average_price": 5862 + "average_price": 5862, }, { "order_id": "3333333333", @@ -128,10 +137,11 @@ "product": "NRML", "order_type": "LIMIT", "quantity": 100, - "average_price": 1.5 - }] + "average_price": 1.5, + }, + ] order_book_charges = kite.get_virtual_contract_note(order_book_params) logging.info("Virtual contract note charges: {}".format(order_book_charges)) except Exception as e: - logging.info("Error fetching virtual contract note charges: {}".format(e)) \ No newline at end of file + logging.info("Error fetching virtual contract note charges: {}".format(e)) diff --git a/examples/simple.py b/examples/simple.py index e0ccf600..4cb923b6 100644 --- a/examples/simple.py +++ b/examples/simple.py @@ -23,7 +23,7 @@ transaction_type=kite.TRANSACTION_TYPE_BUY, quantity=1, product=kite.PRODUCT_CNC, - order_type=kite.ORDER_TYPE_MARKET + order_type=kite.ORDER_TYPE_MARKET, ) logging.info("Order placed. ID is: {}".format(order_id)) @@ -41,7 +41,7 @@ tradingsymbol="INF090I01239", transaction_type=kite.TRANSACTION_TYPE_BUY, amount=5000, - tag="mytag" + tag="mytag", ) # Cancel a mutual fund order diff --git a/examples/threaded_ticker.py b/examples/threaded_ticker.py index 7d4b9547..8738433c 100644 --- a/examples/threaded_ticker.py +++ b/examples/threaded_ticker.py @@ -40,7 +40,9 @@ def on_connect(ws, response): # Callback when current connection is closed. def on_close(ws, code, reason): - logging.info("Connection closed: {code} - {reason}".format(code=code, reason=reason)) + logging.info( + "Connection closed: {code} - {reason}".format(code=code, reason=reason) + ) # Callback when connection closed with error. diff --git a/examples/ticker.py b/examples/ticker.py index c2bced83..7a05234f 100644 --- a/examples/ticker.py +++ b/examples/ticker.py @@ -16,9 +16,11 @@ # Initialise kws = KiteTicker("your_api_key", "your_access_token") + def on_ticks(ws, ticks): # noqa # Callback to receive ticks. - logging.info("Ticks: {}".format(ticks)) + logging.debug(f"Ticks : {ticks}") + def on_connect(ws, response): # noqa # Callback on successful connect. @@ -28,8 +30,10 @@ def on_connect(ws, response): # noqa # Set RELIANCE to tick in `full` mode. ws.set_mode(ws.MODE_FULL, [738561]) + def on_order_update(ws, data): - logging.debug("Order update : {}".format(data)) + logging.debug(f"Order update : {data}") + # Assign the callbacks. kws.on_ticks = on_ticks diff --git a/kiteconnect/connect.py b/kiteconnect/connect.py index 399f406f..ba456645 100644 --- a/kiteconnect/connect.py +++ b/kiteconnect/connect.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- """ - connect.py +connect.py - API wrapper for Kite Connect REST APIs. +API wrapper for Kite Connect REST APIs. - :copyright: (c) 2021 by Zerodha Technology. - :license: see LICENSE for details. +:copyright: (c) 2021 by Zerodha Technology. +:license: see LICENSE for details. """ + +from csv import DictReader +from datetime import datetime +from dateutil.parser import parse as dateutil_parse +from hashlib import sha256 +from typing import Any, Callable, Dict, List, Literal, Optional, TypedDict, Union +from requests import Session +from requests.adapters import HTTPAdapter from six import StringIO, PY2 -from six.moves.urllib.parse import urljoin -import csv +from urllib.parse import urljoin import json -import dateutil.parser -import hashlib import logging -import datetime -import requests import warnings from .__version__ import __version__, __title__ @@ -24,6 +27,14 @@ log = logging.getLogger(__name__) +class Order(TypedDict): + transaction_type: Literal["BUY", "SELL"] + quantity: float + price: float + order_type: Optional[Literal["MARKET", "LIMIT", "SL-M", "SL"]] + product: Optional[Literal["MIS", "CNC", "NRML", "CO"]] + + class KiteConnect(object): """ The Kite Connect API wrapper class. @@ -112,68 +123,60 @@ class KiteConnect(object): "user.profile": "/user/profile", "user.margins": "/user/margins", "user.margins.segment": "/user/margins/{segment}", - "orders": "/orders", "trades": "/trades", - "order.info": "/orders/{order_id}", "order.place": "/orders/{variety}", "order.modify": "/orders/{variety}/{order_id}", "order.cancel": "/orders/{variety}/{order_id}", "order.trades": "/orders/{order_id}/trades", - "portfolio.positions": "/portfolio/positions", "portfolio.holdings": "/portfolio/holdings", "portfolio.holdings.auction": "/portfolio/holdings/auctions", "portfolio.positions.convert": "/portfolio/positions", - # MF api endpoints "mf.orders": "/mf/orders", "mf.order.info": "/mf/orders/{order_id}", "mf.order.place": "/mf/orders", "mf.order.cancel": "/mf/orders/{order_id}", - "mf.sips": "/mf/sips", "mf.sip.info": "/mf/sips/{sip_id}", "mf.sip.place": "/mf/sips", "mf.sip.modify": "/mf/sips/{sip_id}", "mf.sip.cancel": "/mf/sips/{sip_id}", - "mf.holdings": "/mf/holdings", "mf.instruments": "/mf/instruments", - "market.instruments.all": "/instruments", "market.instruments": "/instruments/{exchange}", "market.margins": "/margins/{segment}", "market.historical": "/instruments/historical/{instrument_token}/{interval}", "market.trigger_range": "/instruments/trigger_range/{transaction_type}", - "market.quote": "/quote", "market.quote.ohlc": "/quote/ohlc", "market.quote.ltp": "/quote/ltp", - # GTT endpoints "gtt": "/gtt/triggers", "gtt.place": "/gtt/triggers", "gtt.info": "/gtt/triggers/{trigger_id}", "gtt.modify": "/gtt/triggers/{trigger_id}", "gtt.delete": "/gtt/triggers/{trigger_id}", - # Margin computation endpoints "order.margins": "/margins/orders", "order.margins.basket": "/margins/basket", "order.contract_note": "/charges/orders", } - def __init__(self, - api_key, - access_token=None, - root=None, - debug=False, - timeout=None, - proxies=None, - pool=None, - disable_ssl=False): + def __init__( + self, + api_key: str, + access_token: Optional[str] = None, + root: Optional[str] = None, + debug: bool = False, + timeout: Optional[float] = None, + proxies: Optional[Dict[str, str]] = None, + pool: Optional[Dict[str, str]] = None, + disable_ssl: bool = False, + ): """ Initialise a new Kite Connect client instance. @@ -208,15 +211,14 @@ def __init__(self, # Create requests session by default # Same session to be used by pool connections - self.reqsession = requests.Session() + self.reqsession = Session() if pool: - reqadapter = requests.adapters.HTTPAdapter(**pool) + reqadapter = HTTPAdapter(**pool) self.reqsession.mount("https://", reqadapter) # disable requests SSL warning - requests.packages.urllib3.disable_warnings() - def set_session_expiry_hook(self, method): + def set_session_expiry_hook(self, method: Callable[[]]): """ Set a callback hook for session (`TokenError` -- timeout, expiry etc.) errors. @@ -236,15 +238,15 @@ def set_session_expiry_hook(self, method): self.session_expiry_hook = method - def set_access_token(self, access_token): + def set_access_token(self, access_token: str): """Set the `access_token` received after a successful authentication.""" self.access_token = access_token def login_url(self): """Get the remote login url to which a user should be redirected to initiate the login flow.""" - return "%s?api_key=%s&v=%s" % (self._default_login_uri, self.api_key, self.kite_header_version) + return f"{self._default_login_uri}?api_key={self.api_key}&v={self.kite_header_version}" - def generate_session(self, request_token, api_secret): + def generate_session(self, request_token: str, api_secret: str): """ Generate user session details like `access_token` etc by exchanging `request_token`. Access token is automatically set if the session is retrieved successfully. @@ -257,68 +259,84 @@ def generate_session(self, request_token, api_secret): - `request_token` is the token obtained from the GET paramers after a successful login redirect. - `api_secret` is the API api_secret issued with the API key. """ - h = hashlib.sha256(self.api_key.encode("utf-8") + request_token.encode("utf-8") + api_secret.encode("utf-8")) + h = sha256( + self.api_key.encode("utf-8") + + request_token.encode("utf-8") + + api_secret.encode("utf-8") + ) checksum = h.hexdigest() - resp = self._post("api.token", params={ - "api_key": self.api_key, - "request_token": request_token, - "checksum": checksum - }) + resp = self._post( + "api.token", + params={ + "api_key": self.api_key, + "request_token": request_token, + "checksum": checksum, + }, + ) if "access_token" in resp: self.set_access_token(resp["access_token"]) if resp["login_time"] and len(resp["login_time"]) == 19: - resp["login_time"] = dateutil.parser.parse(resp["login_time"]) + resp["login_time"] = dateutil_parse(resp["login_time"]) return resp - def invalidate_access_token(self, access_token=None): + def invalidate_access_token(self, access_token: Optional[str] = None): """ Kill the session by invalidating the access token. - `access_token` to invalidate. Default is the active `access_token`. """ access_token = access_token or self.access_token - return self._delete("api.token.invalidate", params={ - "api_key": self.api_key, - "access_token": access_token - }) - - def renew_access_token(self, refresh_token, api_secret): + if access_token is None: + raise ValueError("`access_token` cannot be None") + return self._delete( + "api.token.invalidate", + params={"api_key": self.api_key, "access_token": access_token}, + ) + + def renew_access_token(self, refresh_token: str, api_secret: str): """ Renew expired `refresh_token` using valid `refresh_token`. - `refresh_token` is the token obtained from previous successful login flow. - `api_secret` is the API api_secret issued with the API key. """ - h = hashlib.sha256(self.api_key.encode("utf-8") + refresh_token.encode("utf-8") + api_secret.encode("utf-8")) + h = sha256( + self.api_key.encode("utf-8") + + refresh_token.encode("utf-8") + + api_secret.encode("utf-8") + ) checksum = h.hexdigest() - resp = self._post("api.token.renew", params={ - "api_key": self.api_key, - "refresh_token": refresh_token, - "checksum": checksum - }) + resp = self._post( + "api.token.renew", + params={ + "api_key": self.api_key, + "refresh_token": refresh_token, + "checksum": checksum, + }, + ) if "access_token" in resp: self.set_access_token(resp["access_token"]) return resp - def invalidate_refresh_token(self, refresh_token): + def invalidate_refresh_token(self, refresh_token: str): """ Invalidate refresh token. - `refresh_token` is the token which is used to renew access token. """ - return self._delete("api.token.invalidate", params={ - "api_key": self.api_key, - "refresh_token": refresh_token - }) + return self._delete( + "api.token.invalidate", + params={"api_key": self.api_key, "refresh_token": refresh_token}, + ) - def margins(self, segment=None): + def margins(self, segment: Optional[Literal["equity", "commodity"]] = None): """Get account balance and cash margin details for a particular segment. - `segment` is the trading segment (eg: equity or commodity) @@ -333,95 +351,121 @@ def profile(self): return self._get("user.profile") # orders - def place_order(self, - variety, - exchange, - tradingsymbol, - transaction_type, - quantity, - product, - order_type, - price=None, - validity=None, - validity_ttl=None, - disclosed_quantity=None, - trigger_price=None, - iceberg_legs=None, - iceberg_quantity=None, - auction_number=None, - tag=None): + def place_order( + self, + variety: Literal["regular", "co", "amo", "iceberg", "auction"], + exchange: Literal["NSE", "BSE", "NFO", "CDS", "BFO", "MCX", "BCD"], + tradingsymbol: str, + transaction_type: Literal["BUY", "SELL"], + quantity: int, + product: Literal["MIS", "CNC", "NRML", "CO"], + order_type: Literal["MARKET", "LIMIT", "SL-M", "SL"], + price: Optional[float] = None, + validity: Optional[Literal["DAY", "IOC", "TTL"]] = None, + validity_ttl: Optional[int] = None, + disclosed_quantity: Optional[int] = None, + trigger_price: Optional[float] = None, + iceberg_legs: Optional[int] = None, + iceberg_quantity: Optional[int] = None, + auction_number: Optional[int] = None, + tag: Optional[str] = None, + ): """Place an order.""" params = locals() - del (params["self"]) + del params["self"] for k in list(params.keys()): if params[k] is None: - del (params[k]) - - return self._post("order.place", - url_args={"variety": variety}, - params=params)["order_id"] - - def modify_order(self, - variety, - order_id, - parent_order_id=None, - quantity=None, - price=None, - order_type=None, - trigger_price=None, - validity=None, - disclosed_quantity=None): + del params[k] + + return self._post("order.place", url_args={"variety": variety}, params=params)[ + "order_id" + ] + + def modify_order( + self, + variety: Literal["regular", "co", "amo", "iceberg", "auction"], + order_id: str, + parent_order_id: Optional[str] = None, + quantity: Optional[int] = None, + price: Optional[float] = None, + order_type: Optional[Literal["MARKET", "LIMIT", "SL-M", "SL"]] = None, + trigger_price: Optional[float] = None, + validity: Optional[Literal["DAY", "IOC", "TTL"]] = None, + validity_ttl: Optional[int] = None, + disclosed_quantity: Optional[int] = None, + ): """Modify an open order.""" params = locals() - del (params["self"]) + del params["self"] for k in list(params.keys()): if params[k] is None: - del (params[k]) - - return self._put("order.modify", - url_args={"variety": variety, "order_id": order_id}, - params=params)["order_id"] - - def cancel_order(self, variety, order_id, parent_order_id=None): + del params[k] + + return self._put( + "order.modify", + url_args={"variety": variety, "order_id": order_id}, + params=params, + )["order_id"] + + def cancel_order( + self, + variety: Literal["regular", "co", "amo", "iceberg", "auction"], + order_id: str, + parent_order_id: Optional[str] = None, + ): """Cancel an order.""" - return self._delete("order.cancel", - url_args={"variety": variety, "order_id": order_id}, - params={"parent_order_id": parent_order_id})["order_id"] - - def exit_order(self, variety, order_id, parent_order_id=None): + return self._delete( + "order.cancel", + url_args={"variety": variety, "order_id": order_id}, + params={"parent_order_id": parent_order_id} if parent_order_id else None, + )["order_id"] + + def exit_order( + self, + variety: Literal["regular", "co", "amo", "iceberg", "auction"], + order_id: str, + parent_order_id: Optional[str] = None, + ): """Exit a CO order.""" return self.cancel_order(variety, order_id, parent_order_id=parent_order_id) - def _format_response(self, data): + def _format_response(self, data: Union[List[Dict[str, Any]], Dict[str, Any]]): """Parse and format responses.""" - if type(data) == list: - _list = data - elif type(data) == dict: - _list = [data] + _list = data if isinstance(data, list) else [data] for item in _list: # Convert date time string to datetime object - for field in ["order_timestamp", "exchange_timestamp", "created", "last_instalment", "fill_timestamp", "timestamp", "last_trade_time"]: + for field in [ + "order_timestamp", + "exchange_timestamp", + "created", + "last_instalment", + "fill_timestamp", + "timestamp", + "last_trade_time", + ]: if item.get(field) and len(item[field]) == 19: - item[field] = dateutil.parser.parse(item[field]) + item[field] = dateutil_parse(item[field]) - return _list[0] if type(data) == dict else _list + return _list[0] if isinstance(data, dict) else _list # orderbook and tradebook def orders(self): """Get list of orders.""" return self._format_response(self._get("orders")) - def order_history(self, order_id): + def order_history(self, order_id: str): """ Get history of individual order. - `order_id` is the ID of the order to retrieve order history. """ - return self._format_response(self._get("order.info", url_args={"order_id": order_id})) + return self._format_response( + self._get("order.info", url_args={"order_id": order_id}) + ) def trades(self): """ @@ -432,13 +476,15 @@ def trades(self): """ return self._format_response(self._get("trades")) - def order_trades(self, order_id): + def order_trades(self, order_id: str): """ Retrieve the list of trades executed for a particular order. - `order_id` is the ID of the order to retrieve trade history. """ - return self._format_response(self._get("order.trades", url_args={"order_id": order_id})) + return self._format_response( + self._get("order.trades", url_args={"order_id": order_id}) + ) def positions(self): """Retrieve the list of positions.""" @@ -449,99 +495,122 @@ def holdings(self): return self._get("portfolio.holdings") def get_auction_instruments(self): - """ Retrieves list of available instruments for a auction session """ + """Retrieves list of available instruments for a auction session""" return self._get("portfolio.holdings.auction") - def convert_position(self, - exchange, - tradingsymbol, - transaction_type, - position_type, - quantity, - old_product, - new_product): + def convert_position( + self, + exchange: Literal["NSE", "BSE", "NFO", "CDS", "BFO", "MCX", "BCD"], + tradingsymbol, + transaction_type: Literal["BUY", "SELL"], + position_type: Literal["day", "overnight"], + quantity: int, + old_product: Literal["MIS", "CNC", "NRML", "CO"], + new_product: Literal["MIS", "CNC", "NRML", "CO"], + ): """Modify an open position's product type.""" - return self._put("portfolio.positions.convert", params={ - "exchange": exchange, - "tradingsymbol": tradingsymbol, - "transaction_type": transaction_type, - "position_type": position_type, - "quantity": quantity, - "old_product": old_product, - "new_product": new_product - }) - - def mf_orders(self, order_id=None): + return self._put( + "portfolio.positions.convert", + params={ + "exchange": exchange, + "tradingsymbol": tradingsymbol, + "transaction_type": transaction_type, + "position_type": position_type, + "quantity": quantity, + "old_product": old_product, + "new_product": new_product, + }, + ) + + def mf_orders(self, order_id: Optional[str] = None): """Get all mutual fund orders or individual order info.""" if order_id: - return self._format_response(self._get("mf.order.info", url_args={"order_id": order_id})) + return self._format_response( + self._get("mf.order.info", url_args={"order_id": order_id}) + ) else: return self._format_response(self._get("mf.orders")) - def place_mf_order(self, - tradingsymbol, - transaction_type, - quantity=None, - amount=None, - tag=None): + def place_mf_order( + self, + tradingsymbol: str, + transaction_type: Literal["BUY", "SELL"], + quantity: Optional[int] = None, + amount: Optional[float] = None, + tag: Optional[str] = None, + ): """Place a mutual fund order.""" - return self._post("mf.order.place", params={ - "tradingsymbol": tradingsymbol, - "transaction_type": transaction_type, - "quantity": quantity, - "amount": amount, - "tag": tag - }) - - def cancel_mf_order(self, order_id): + return self._post( + "mf.order.place", + params={ + "tradingsymbol": tradingsymbol, + "transaction_type": transaction_type, + "quantity": quantity, + "amount": amount, + "tag": tag, + }, + ) + + def cancel_mf_order(self, order_id: str): """Cancel a mutual fund order.""" return self._delete("mf.order.cancel", url_args={"order_id": order_id}) - def mf_sips(self, sip_id=None): + def mf_sips(self, sip_id: Optional[str] = None): """Get list of all mutual fund SIP's or individual SIP info.""" if sip_id: - return self._format_response(self._get("mf.sip.info", url_args={"sip_id": sip_id})) + return self._format_response( + self._get("mf.sip.info", url_args={"sip_id": sip_id}) + ) else: return self._format_response(self._get("mf.sips")) - def place_mf_sip(self, - tradingsymbol, - amount, - instalments, - frequency, - initial_amount=None, - instalment_day=None, - tag=None): + def place_mf_sip( + self, + tradingsymbol: str, + amount: float, + instalments: float, + frequency: str, + initial_amount: Optional[float] = None, + instalment_day: Optional[int] = None, + tag: Optional[str] = None, + ): """Place a mutual fund SIP.""" - return self._post("mf.sip.place", params={ - "tradingsymbol": tradingsymbol, - "amount": amount, - "initial_amount": initial_amount, - "instalments": instalments, - "frequency": frequency, - "instalment_day": instalment_day, - "tag": tag - }) - - def modify_mf_sip(self, - sip_id, - amount=None, - status=None, - instalments=None, - frequency=None, - instalment_day=None): + return self._post( + "mf.sip.place", + params={ + "tradingsymbol": tradingsymbol, + "amount": amount, + "initial_amount": initial_amount, + "instalments": instalments, + "frequency": frequency, + "instalment_day": instalment_day, + "tag": tag, + }, + ) + + def modify_mf_sip( + self, + sip_id: str, + amount: Optional[float] = None, + status: Optional[Literal["COMPLETED", "CANCELLED", "PENDING"]] = None, + instalments: Optional[float] = None, + frequency: Optional[str] = None, + instalment_day: Optional[int] = None, + ): """Modify a mutual fund SIP.""" - return self._put("mf.sip.modify", - url_args={"sip_id": sip_id}, - params={ - "amount": amount, - "status": status, - "instalments": instalments, - "frequency": frequency, - "instalment_day": instalment_day - }) - - def cancel_mf_sip(self, sip_id): + return self._put( + "mf.sip.modify", + url_args={"sip_id": sip_id}, + params={ + "amount": amount, + "status": status, + "instalments": instalments, + "frequency": frequency, + "instalment_day": instalment_day, + }, + ) + + def cancel_mf_sip(self, sip_id: str): """Cancel a mutual fund SIP.""" return self._delete("mf.sip.cancel", url_args={"sip_id": sip_id}) @@ -553,7 +622,12 @@ def mf_instruments(self): """Get list of mutual fund instruments.""" return self._parse_mf_instruments(self._get("mf.instruments")) - def instruments(self, exchange=None): + def instruments( + self, + exchange: Optional[ + Literal["NSE", "BSE", "NFO", "CDS", "BFO", "MCX", "BCD"] + ] = None, + ): """ Retrieve the list of market instruments available to trade. @@ -563,9 +637,10 @@ def instruments(self, exchange=None): - `exchange` is specific exchange to fetch (Optional) """ if exchange: - return self._parse_instruments(self._get("market.instruments", url_args={"exchange": exchange})) - else: - return self._parse_instruments(self._get("market.instruments.all")) + return self._parse_instruments( + self._get("market.instruments", url_args={"exchange": exchange}) + ) + return self._parse_instruments(self._get("market.instruments.all")) def quote(self, *instruments): """ @@ -576,7 +651,7 @@ def quote(self, *instruments): ins = list(instruments) # If first element is a list then accept it as instruments list for legacy reason - if len(instruments) > 0 and type(instruments[0]) == list: + if len(instruments) > 0 and isinstance(instruments[0], list): ins = instruments[0] data = self._get("market.quote", params={"i": ins}) @@ -591,7 +666,7 @@ def ohlc(self, *instruments): ins = list(instruments) # If first element is a list then accept it as instruments list for legacy reason - if len(instruments) > 0 and type(instruments[0]) == list: + if len(instruments) > 0 and isinstance(instruments[0], list): ins = instruments[0] return self._get("market.quote.ohlc", params={"i": ins}) @@ -605,12 +680,20 @@ def ltp(self, *instruments): ins = list(instruments) # If first element is a list then accept it as instruments list for legacy reason - if len(instruments) > 0 and type(instruments[0]) == list: + if isinstance(instruments[0], list) and len(instruments) > 0: ins = instruments[0] return self._get("market.quote.ltp", params={"i": ins}) - def historical_data(self, instrument_token, from_date, to_date, interval, continuous=False, oi=False): + def historical_data( + self, + instrument_token: str, + from_date: Union[str, datetime], + to_date: Union[str, datetime], + interval, + continuous: bool = False, + oi: bool = False, + ): """ Retrieve historical data (candles) for an instrument. @@ -626,26 +709,36 @@ def historical_data(self, instrument_token, from_date, to_date, interval, contin - `oi` is a boolean flag to get open interest. """ date_string_format = "%Y-%m-%d %H:%M:%S" - from_date_string = from_date.strftime(date_string_format) if type(from_date) == datetime.datetime else from_date - to_date_string = to_date.strftime(date_string_format) if type(to_date) == datetime.datetime else to_date - - data = self._get("market.historical", - url_args={"instrument_token": instrument_token, "interval": interval}, - params={ - "from": from_date_string, - "to": to_date_string, - "interval": interval, - "continuous": 1 if continuous else 0, - "oi": 1 if oi else 0 - }) + from_date_string = ( + from_date.strftime(date_string_format) + if isinstance(from_date, datetime) + else from_date + ) + to_date_string = ( + to_date.strftime(date_string_format) + if isinstance(to_date, datetime) + else to_date + ) + + data = self._get( + "market.historical", + url_args={"instrument_token": instrument_token, "interval": interval}, + params={ + "from": from_date_string, + "to": to_date_string, + "interval": interval, + "continuous": 1 if continuous else 0, + "oi": 1 if oi else 0, + }, + ) return self._format_historical(data) - def _format_historical(self, data): + def _format_historical(self, data: Dict[str, Any]): records = [] for d in data["candles"]: record = { - "date": dateutil.parser.parse(d[0]), + "date": dateutil_parse(d[0]), "open": d[1], "high": d[2], "low": d[3], @@ -658,32 +751,44 @@ def _format_historical(self, data): return records - def trigger_range(self, transaction_type, *instruments): + def trigger_range(self, transaction_type: Literal["BUY", "SELL"], *instruments): """Retrieve the buy/sell trigger range for Cover Orders.""" ins = list(instruments) # If first element is a list then accept it as instruments list for legacy reason - if len(instruments) > 0 and type(instruments[0]) == list: + if isinstance(instruments[0], list) and len(instruments) > 0: ins = instruments[0] - return self._get("market.trigger_range", - url_args={"transaction_type": transaction_type.lower()}, - params={"i": ins}) + return self._get( + "market.trigger_range", + url_args={"transaction_type": transaction_type.lower()}, + params={"i": ins}, + ) def get_gtts(self): """Fetch list of gtt existing in an account""" return self._get("gtt") - def get_gtt(self, trigger_id): + def get_gtt(self, trigger_id: str): """Fetch details of a GTT""" return self._get("gtt.info", url_args={"trigger_id": trigger_id}) - def _get_gtt_payload(self, trigger_type, tradingsymbol, exchange, trigger_values, last_price, orders): + def _get_gtt_payload( + self, + trigger_type: Literal["single", "two-leg"], + tradingsymbol: str, + exchange: Literal["NSE", "BSE", "NFO", "CDS", "BFO", "MCX", "BCD"], + trigger_values: List[Dict[str, float]], + last_price: float, + orders: List[Order], + ): """Get GTT payload""" - if type(trigger_values) != list: + if not isinstance(trigger_values, list): raise ex.InputException("invalid type for `trigger_values`") if trigger_type == self.GTT_TYPE_SINGLE and len(trigger_values) != 1: - raise ex.InputException("invalid `trigger_values` for single leg order type") + raise ex.InputException( + "invalid `trigger_values` for single leg order type" + ) elif trigger_type == self.GTT_TYPE_OCO and len(trigger_values) != 2: raise ex.InputException("invalid `trigger_values` for OCO order type") @@ -697,23 +802,37 @@ def _get_gtt_payload(self, trigger_type, tradingsymbol, exchange, trigger_values gtt_orders = [] for o in orders: # Assert required keys inside gtt order. - for req in ["transaction_type", "quantity", "order_type", "product", "price"]: + for req in [ + "transaction_type", + "quantity", + "order_type", + "product", + "price", + ]: if req not in o: - raise ex.InputException("`{req}` missing inside orders".format(req=req)) - gtt_orders.append({ - "exchange": exchange, - "tradingsymbol": tradingsymbol, - "transaction_type": o["transaction_type"], - "quantity": int(o["quantity"]), - "order_type": o["order_type"], - "product": o["product"], - "price": float(o["price"]), - }) + raise ex.InputException(f"`{req}` missing inside orders") + gtt_orders.append( + { + "exchange": exchange, + "tradingsymbol": tradingsymbol, + "transaction_type": o["transaction_type"], + "quantity": int(o["quantity"]), + "order_type": o["order_type"], + "product": o["product"], + "price": float(o["price"]), + } + ) return condition, gtt_orders def place_gtt( - self, trigger_type, tradingsymbol, exchange, trigger_values, last_price, orders + self, + trigger_type: Literal["single", "two-leg"], + tradingsymbol: str, + exchange: Literal["NSE", "BSE", "NFO", "CDS", "BFO", "MCX", "BCD"], + trigger_values: List[Dict[str, float]], + last_price: float, + orders: List[Order], ): """ Place GTT order @@ -730,15 +849,28 @@ def place_gtt( """ # Validations. assert trigger_type in [self.GTT_TYPE_OCO, self.GTT_TYPE_SINGLE] - condition, gtt_orders = self._get_gtt_payload(trigger_type, tradingsymbol, exchange, trigger_values, last_price, orders) - - return self._post("gtt.place", params={ - "condition": json.dumps(condition), - "orders": json.dumps(gtt_orders), - "type": trigger_type}) + condition, gtt_orders = self._get_gtt_payload( + trigger_type, tradingsymbol, exchange, trigger_values, last_price, orders + ) + + return self._post( + "gtt.place", + params={ + "condition": json.dumps(condition), + "orders": json.dumps(gtt_orders), + "type": trigger_type, + }, + ) def modify_gtt( - self, trigger_id, trigger_type, tradingsymbol, exchange, trigger_values, last_price, orders + self, + trigger_id: str, + trigger_type: Literal["single", "two-leg"], + tradingsymbol: str, + exchange: Literal["NSE", "BSE", "NFO", "CDS", "BFO", "MCX", "BCD"], + trigger_values: List[Dict[str, float]], + last_price: float, + orders: List[Order], ): """ Modify GTT order @@ -753,20 +885,25 @@ def modify_gtt( - `quantity` Quantity to transact - `price` The min or max price to execute the order at (for LIMIT orders) """ - condition, gtt_orders = self._get_gtt_payload(trigger_type, tradingsymbol, exchange, trigger_values, last_price, orders) - - return self._put("gtt.modify", - url_args={"trigger_id": trigger_id}, - params={ - "condition": json.dumps(condition), - "orders": json.dumps(gtt_orders), - "type": trigger_type}) - - def delete_gtt(self, trigger_id): + condition, gtt_orders = self._get_gtt_payload( + trigger_type, tradingsymbol, exchange, trigger_values, last_price, orders + ) + + return self._put( + "gtt.modify", + url_args={"trigger_id": trigger_id}, + params={ + "condition": json.dumps(condition), + "orders": json.dumps(gtt_orders), + "type": trigger_type, + }, + ) + + def delete_gtt(self, trigger_id: str): """Delete a GTT order.""" return self._delete("gtt.delete", url_args={"trigger_id": trigger_id}) - def order_margins(self, params): + def order_margins(self, params: List[Dict[str, Any]]): """ Calculate margins for requested order list considering the existing positions and open orders @@ -774,7 +911,12 @@ def order_margins(self, params): """ return self._post("order.margins", params=params, is_json=True) - def basket_order_margins(self, params, consider_positions=True, mode=None): + def basket_order_margins( + self, + params: List[Dict[str, Any]], + consider_positions: bool = True, + mode: Optional[Literal["compact", "full"]] = None, + ): """ Calculate total margins required for basket of orders including margin benefits @@ -782,34 +924,34 @@ def basket_order_margins(self, params, consider_positions=True, mode=None): - `consider_positions` is a boolean to consider users positions - `mode` is margin response mode type. compact - Compact mode will only give the total margins """ - return self._post("order.margins.basket", - params=params, - is_json=True, - query_params={'consider_positions': consider_positions, 'mode': mode}) + return self._post( + "order.margins.basket", + params=params, + is_json=True, + query_params={"consider_positions": consider_positions, "mode": mode}, + ) def get_virtual_contract_note(self, params): """ Calculates detailed charges order-wise for the order book - `params` is list of orders to fetch charges detail """ - return self._post("order.contract_note", - params=params, - is_json=True) + return self._post("order.contract_note", params=params, is_json=True) def _warn(self, message): - """ Add deprecation warning message """ - warnings.simplefilter('always', DeprecationWarning) + """Add deprecation warning message""" + warnings.simplefilter("always", DeprecationWarning) warnings.warn(message, DeprecationWarning) def _parse_instruments(self, data): # decode to string for Python 3 d = data # Decode unicode data - if not PY2 and type(d) == bytes: + if not PY2 and isinstance(d, bytes): d = data.decode("utf-8").strip() records = [] - reader = csv.DictReader(StringIO(d)) + reader = DictReader(StringIO(d)) for row in reader: row["instrument_token"] = int(row["instrument_token"]) @@ -820,7 +962,7 @@ def _parse_instruments(self, data): # Parse date if len(row["expiry"]) == 10: - row["expiry"] = dateutil.parser.parse(row["expiry"]).date() + row["expiry"] = dateutil_parse(row["expiry"]).date() records.append(row) @@ -829,25 +971,31 @@ def _parse_instruments(self, data): def _parse_mf_instruments(self, data): # decode to string for Python 3 d = data - if not PY2 and type(d) == bytes: + if not PY2 and isinstance(d, bytes): d = data.decode("utf-8").strip() records = [] - reader = csv.DictReader(StringIO(d)) + reader = DictReader(StringIO(d)) for row in reader: row["minimum_purchase_amount"] = float(row["minimum_purchase_amount"]) row["purchase_amount_multiplier"] = float(row["purchase_amount_multiplier"]) - row["minimum_additional_purchase_amount"] = float(row["minimum_additional_purchase_amount"]) - row["minimum_redemption_quantity"] = float(row["minimum_redemption_quantity"]) - row["redemption_quantity_multiplier"] = float(row["redemption_quantity_multiplier"]) + row["minimum_additional_purchase_amount"] = float( + row["minimum_additional_purchase_amount"] + ) + row["minimum_redemption_quantity"] = float( + row["minimum_redemption_quantity"] + ) + row["redemption_quantity_multiplier"] = float( + row["redemption_quantity_multiplier"] + ) row["purchase_allowed"] = bool(int(row["purchase_allowed"])) row["redemption_allowed"] = bool(int(row["redemption_allowed"])) row["last_price"] = float(row["last_price"]) # Parse date if len(row["last_price_date"]) == 10: - row["last_price_date"] = dateutil.parser.parse(row["last_price_date"]).date() + row["last_price_date"] = dateutil_parse(row["last_price_date"]).date() records.append(row) @@ -856,23 +1004,88 @@ def _parse_mf_instruments(self, data): def _user_agent(self): return (__title__ + "-python/").capitalize() + __version__ - def _get(self, route, url_args=None, params=None, is_json=False): + def _get( + self, + route: str, + url_args: Optional[Dict[str, str]] = None, + params: Optional[Dict[str, Any]] = None, + is_json: bool = False, + ) -> Dict[str, Any]: """Alias for sending a GET request.""" - return self._request(route, "GET", url_args=url_args, params=params, is_json=is_json) + resp = self._request( + route, "GET", url_args=url_args, params=params, is_json=is_json + ) + if not isinstance(resp, dict): + raise ex.DataException("The data fetched is not of the correct data type") + return resp - def _post(self, route, url_args=None, params=None, is_json=False, query_params=None): + def _post( + self, + route: str, + url_args: Optional[Dict[str, str]] = None, + params: Optional[Union[Dict[str, Any], List[Dict[str, str]]]] = None, + is_json: bool = False, + query_params: Optional[Dict[str, Any]] = None, + ) -> Dict[str, Any]: """Alias for sending a POST request.""" - return self._request(route, "POST", url_args=url_args, params=params, is_json=is_json, query_params=query_params) + resp = self._request( + route, + "POST", + url_args=url_args, + params=params, + is_json=is_json, + query_params=query_params, + ) + if not isinstance(resp, dict): + raise ex.DataException("The data fetched is not of the correct data type") + return resp - def _put(self, route, url_args=None, params=None, is_json=False, query_params=None): + def _put( + self, + route: str, + url_args: Optional[Dict[str, str]] = None, + params: Optional[Dict[str, Any]] = None, + is_json=False, + query_params: Optional[Dict[str, Any]] = None, + ) -> Dict[str, Any]: """Alias for sending a PUT request.""" - return self._request(route, "PUT", url_args=url_args, params=params, is_json=is_json, query_params=query_params) + resp = self._request( + route, + "PUT", + url_args=url_args, + params=params, + is_json=is_json, + query_params=query_params, + ) + if not isinstance(resp, dict): + raise ex.DataException("The data fetched is not of the correct data type") + return resp - def _delete(self, route, url_args=None, params=None, is_json=False): + def _delete( + self, + route, + url_args: Optional[Dict[str, str]] = None, + params: Optional[Dict[str, str]] = None, + is_json: bool = False, + ) -> Dict[str, Any]: """Alias for sending a DELETE request.""" - return self._request(route, "DELETE", url_args=url_args, params=params, is_json=is_json) + resp = self._request( + route, "DELETE", url_args=url_args, params=params, is_json=is_json + ) + if not isinstance(resp, dict): + raise ex.DataException("The data fetched is not of the correct data type") + return resp - def _request(self, route, method, url_args=None, params=None, is_json=False, query_params=None): + def _request( + self, + route: str, + method: Literal["GET", "POST", "PUT", "DELETE"], + *, + url_args: Optional[Dict[str, str]] = None, + params: Optional[Union[Dict[str, str], List[Dict[str, str]]]] = None, + is_json: bool = False, + query_params: Optional[Union[Dict[str, str], List[Dict[str, str]]]] = None, + ): """Make an HTTP request.""" # Form a restful URL if url_args: @@ -885,51 +1098,58 @@ def _request(self, route, method, url_args=None, params=None, is_json=False, que # Custom headers headers = { "X-Kite-Version": self.kite_header_version, - "User-Agent": self._user_agent() + "User-Agent": self._user_agent(), } if self.api_key and self.access_token: # set authorization header auth_header = self.api_key + ":" + self.access_token - headers["Authorization"] = "token {}".format(auth_header) + headers["Authorization"] = f"token {auth_header}" if self.debug: - log.debug("Request: {method} {url} {params} {headers}".format(method=method, url=url, params=params, headers=headers)) + log.debug(f"Request: {method} {url} {params} {headers}") # prepare url query params if method in ["GET", "DELETE"]: query_params = params try: - r = self.reqsession.request(method, - url, - json=params if (method in ["POST", "PUT"] and is_json) else None, - data=params if (method in ["POST", "PUT"] and not is_json) else None, - params=query_params, - headers=headers, - verify=not self.disable_ssl, - allow_redirects=True, - timeout=self.timeout, - proxies=self.proxies) + r = self.reqsession.request( + method, + url, + json=params if (method in ["POST", "PUT"] and is_json) else None, + data=params if (method in ["POST", "PUT"] and not is_json) else None, + params=query_params, + headers=headers, + verify=not self.disable_ssl, + allow_redirects=True, + timeout=self.timeout, + proxies=self.proxies, + ) # Any requests lib related exceptions are raised here - https://requests.readthedocs.io/en/latest/api/#exceptions except Exception as e: raise e if self.debug: - log.debug("Response: {code} {content}".format(code=r.status_code, content=r.content)) + log.debug(f"Response: {r.status_code} {r.content}") # Validate the content type. if "json" in r.headers["content-type"]: try: data = r.json() except ValueError: - raise ex.DataException("Couldn't parse the JSON response received from the server: {content}".format( - content=r.content)) + raise ex.DataException( + f"Couldn't parse the JSON response received from the server: {r.content}" + ) # api error if data.get("status") == "error" or data.get("error_type"): # Call session hook if its registered and TokenException is raised - if self.session_expiry_hook and r.status_code == 403 and data["error_type"] == "TokenException": + if ( + self.session_expiry_hook + and r.status_code == 403 + and data["error_type"] == "TokenException" + ): self.session_expiry_hook() # native Kite errors @@ -939,7 +1159,6 @@ def _request(self, route, method, url_args=None, params=None, is_json=False, que return data["data"] elif "csv" in r.headers["content-type"]: return r.content - else: - raise ex.DataException("Unknown Content-Type ({content_type}) with response: ({content})".format( - content_type=r.headers["content-type"], - content=r.content)) + raise ex.DataException( + f"Unknown Content-Type ({r.headers['content-type']}) with response: ({r.content})" + ) diff --git a/kiteconnect/exceptions.py b/kiteconnect/exceptions.py index 239ea6e3..d6cb7ae2 100644 --- a/kiteconnect/exceptions.py +++ b/kiteconnect/exceptions.py @@ -1,11 +1,11 @@ # -*- coding: utf-8 -*- """ - exceptions.py +exceptions.py - Exceptions raised by the Kite Connect client. +Exceptions raised by the Kite Connect client. - :copyright: (c) 2021 by Zerodha Technology. - :license: see LICENSE for details. +:copyright: (c) 2021 by Zerodha Technology. +:license: see LICENSE for details. """ @@ -18,7 +18,7 @@ class KiteException(Exception): and `.message` (error text). """ - def __init__(self, message, code=500): + def __init__(self, message: str, code: int = 500): """Initialize the exception.""" super(KiteException, self).__init__(message) self.code = code @@ -27,7 +27,7 @@ def __init__(self, message, code=500): class GeneralException(KiteException): """An unclassified, general error. Default code is 500.""" - def __init__(self, message, code=500): + def __init__(self, message: str, code: int = 500): """Initialize the exception.""" super(GeneralException, self).__init__(message, code) @@ -35,7 +35,7 @@ def __init__(self, message, code=500): class TokenException(KiteException): """Represents all token and authentication related errors. Default code is 403.""" - def __init__(self, message, code=403): + def __init__(self, message: str, code: int = 403): """Initialize the exception.""" super(TokenException, self).__init__(message, code) @@ -43,7 +43,7 @@ def __init__(self, message, code=403): class PermissionException(KiteException): """Represents permission denied exceptions for certain calls. Default code is 403.""" - def __init__(self, message, code=403): + def __init__(self, message: str, code: int = 403): """Initialize the exception.""" super(PermissionException, self).__init__(message, code) @@ -51,7 +51,7 @@ def __init__(self, message, code=403): class OrderException(KiteException): """Represents all order placement and manipulation errors. Default code is 500.""" - def __init__(self, message, code=500): + def __init__(self, message: str, code: int = 500): """Initialize the exception.""" super(OrderException, self).__init__(message, code) @@ -59,7 +59,7 @@ def __init__(self, message, code=500): class InputException(KiteException): """Represents user input errors such as missing and invalid parameters. Default code is 400.""" - def __init__(self, message, code=400): + def __init__(self, message: str, code: int = 400): """Initialize the exception.""" super(InputException, self).__init__(message, code) @@ -67,7 +67,7 @@ def __init__(self, message, code=400): class DataException(KiteException): """Represents a bad response from the backend Order Management System (OMS). Default code is 502.""" - def __init__(self, message, code=502): + def __init__(self, message: str, code: int = 502): """Initialize the exception.""" super(DataException, self).__init__(message, code) @@ -75,6 +75,6 @@ def __init__(self, message, code=502): class NetworkException(KiteException): """Represents a network issue between Kite and the backend Order Management System (OMS). Default code is 503.""" - def __init__(self, message, code=503): + def __init__(self, message: str, code: int = 503): """Initialize the exception.""" super(NetworkException, self).__init__(message, code) diff --git a/kiteconnect/ticker.py b/kiteconnect/ticker.py index ad858d7e..a37c4856 100644 --- a/kiteconnect/ticker.py +++ b/kiteconnect/ticker.py @@ -1,213 +1,36 @@ # -*- coding: utf-8 -*- """ - ticker.py +ticker.py - Websocket implementation for kite ticker +Modern WebSocket implementation for Kite ticker using Python's websockets library - :copyright: (c) 2021 by Zerodha Technology Pvt. Ltd. - :license: see LICENSE for details. +:copyright: (c) 2021 by Zerodha Technology Pvt. Ltd. +:license: see LICENSE for details. """ -import six -import sys -import time -import json -import struct -import logging -import threading + +from asyncio import iscoroutinefunction, sleep +from json import JSONDecodeError, loads, dumps +from logging import getLogger +from ssl import create_default_context +from struct import unpack from datetime import datetime -from twisted.internet import reactor, ssl -from twisted.python import log as twisted_log -from twisted.internet.protocol import ReconnectingClientFactory -from autobahn.twisted.websocket import WebSocketClientProtocol, \ - WebSocketClientFactory, connectWS +from typing import Any, Dict, List, Literal, Optional, Callable +from urllib.parse import urlencode +from websockets import ClientConnection, connect +from websockets.exceptions import ConnectionClosed from .__version__ import __version__, __title__ -log = logging.getLogger(__name__) - - -class KiteTickerClientProtocol(WebSocketClientProtocol): - """Kite ticker autobahn WebSocket protocol.""" - - PING_INTERVAL = 2.5 - KEEPALIVE_INTERVAL = 5 - - _next_ping = None - _next_pong_check = None - _last_pong_time = None - _last_ping_time = None - - def __init__(self, *args, **kwargs): - """Initialize protocol with all options passed from factory.""" - super(KiteTickerClientProtocol, self).__init__(*args, **kwargs) - - # Overide method - def onConnect(self, response): # noqa - """Called when WebSocket server connection was established""" - self.factory.ws = self - - if self.factory.on_connect: - self.factory.on_connect(self, response) - - # Reset reconnect on successful reconnect - self.factory.resetDelay() - - # Overide method - def onOpen(self): # noqa - """Called when the initial WebSocket opening handshake was completed.""" - # send ping - self._loop_ping() - # init last pong check after X seconds - self._loop_pong_check() - - if self.factory.on_open: - self.factory.on_open(self) - - # Overide method - def onMessage(self, payload, is_binary): # noqa - """Called when text or binary message is received.""" - if self.factory.on_message: - self.factory.on_message(self, payload, is_binary) - - # Overide method - def onClose(self, was_clean, code, reason): # noqa - """Called when connection is closed.""" - if not was_clean: - if self.factory.on_error: - self.factory.on_error(self, code, reason) - - if self.factory.on_close: - self.factory.on_close(self, code, reason) +log = getLogger(__name__) - # Cancel next ping and timer - self._last_ping_time = None - self._last_pong_time = None - if self._next_ping: - self._next_ping.cancel() - - if self._next_pong_check: - self._next_pong_check.cancel() - - def onPong(self, response): # noqa - """Called when pong message is received.""" - if self._last_pong_time and self.factory.debug: - log.debug("last pong was {} seconds back.".format(time.time() - self._last_pong_time)) - - self._last_pong_time = time.time() - - if self.factory.debug: - log.debug("pong => {}".format(response)) - - """ - Custom helper and exposed methods. - """ - - def _loop_ping(self): # noqa - """Start a ping loop where it sends ping message every X seconds.""" - if self.factory.debug: - if self._last_ping_time: - log.debug("last ping was {} seconds back.".format(time.time() - self._last_ping_time)) - - # Set current time as last ping time - self._last_ping_time = time.time() - - # Call self after X seconds - self._next_ping = self.factory.reactor.callLater(self.PING_INTERVAL, self._loop_ping) - - def _loop_pong_check(self): - """ - Timer sortof to check if connection is still there. - - Checks last pong message time and disconnects the existing connection to make sure it doesn't become a ghost connection. - """ - if self._last_pong_time: - # No pong message since long time, so init reconnect - last_pong_diff = time.time() - self._last_pong_time - if last_pong_diff > (2 * self.PING_INTERVAL): - if self.factory.debug: - log.debug("Last pong was {} seconds ago. So dropping connection to reconnect.".format( - last_pong_diff)) - # drop existing connection to avoid ghost connection - self.dropConnection(abort=True) - - # Call self after X seconds - self._next_pong_check = self.factory.reactor.callLater(self.PING_INTERVAL, self._loop_pong_check) - - -class KiteTickerClientFactory(WebSocketClientFactory, ReconnectingClientFactory): - """Autobahn WebSocket client factory to implement reconnection and custom callbacks.""" - - protocol = KiteTickerClientProtocol - maxDelay = 5 - maxRetries = 10 - - _last_connection_time = None - - def __init__(self, *args, **kwargs): - """Initialize with default callback method values.""" - self.debug = False - self.ws = None - self.on_open = None - self.on_error = None - self.on_close = None - self.on_message = None - self.on_connect = None - self.on_reconnect = None - self.on_noreconnect = None - - super(KiteTickerClientFactory, self).__init__(*args, **kwargs) - - def startedConnecting(self, connector): # noqa - """On connecting start or reconnection.""" - if not self._last_connection_time and self.debug: - log.debug("Start WebSocket connection.") - - self._last_connection_time = time.time() - - def clientConnectionFailed(self, connector, reason): # noqa - """On connection failure (When connect request fails)""" - if self.retries > 0: - log.error("Retrying connection. Retry attempt count: {}. Next retry in around: {} seconds".format(self.retries, int(round(self.delay)))) - - # on reconnect callback - if self.on_reconnect: - self.on_reconnect(self.retries) - - # Retry the connection - self.retry(connector) - self.send_noreconnect() - - def clientConnectionLost(self, connector, reason): # noqa - """On connection lost (When ongoing connection got disconnected).""" - if self.retries > 0: - # on reconnect callback - if self.on_reconnect: - self.on_reconnect(self.retries) - - # Retry the connection - self.retry(connector) - self.send_noreconnect() - - def send_noreconnect(self): - """Callback `no_reconnect` if max retries are exhausted.""" - if self.maxRetries is not None and (self.retries > self.maxRetries): - if self.debug: - log.debug("Maximum retries ({}) exhausted.".format(self.maxRetries)) - # Stop the loop for exceeding max retry attempts - self.stop() - - if self.on_noreconnect: - self.on_noreconnect() - - -class KiteTicker(object): +class KiteTicker: """ - The WebSocket client for connecting to Kite Connect's streaming quotes service. + Modern WebSocket client for connecting to Kite Connect's streaming quotes service. Getting started: --------------- - #!python + import asyncio import logging from kiteconnect import KiteTicker @@ -216,144 +39,28 @@ class KiteTicker(object): # Initialise kws = KiteTicker("your_api_key", "your_access_token") - def on_ticks(ws, ticks): + async def on_ticks(ws, ticks): # Callback to receive ticks. - logging.debug("Ticks: {}".format(ticks)) + logging.debug(f"Ticks: {ticks}") - def on_connect(ws, response): + async def on_connect(ws): # Callback on successful connect. # Subscribe to a list of instrument_tokens (RELIANCE and ACC here). - ws.subscribe([738561, 5633]) - + await ws.subscribe([738561, 5633]) # Set RELIANCE to tick in `full` mode. - ws.set_mode(ws.MODE_FULL, [738561]) + await ws.set_mode(ws.MODE_FULL, [738561]) - def on_close(ws, code, reason): - # On connection close stop the event loop. - # Reconnection will not happen after executing `ws.stop()` - ws.stop() + async def on_close(ws, code, reason): + # On connection close + logging.info(f"Connection closed: {code} - {reason}") # Assign the callbacks. kws.on_ticks = on_ticks kws.on_connect = on_connect kws.on_close = on_close - # Infinite loop on the main thread. Nothing after this will run. - # You have to use the pre-defined callbacks to manage subscriptions. - kws.connect() - - Callbacks - --------- - In below examples `ws` is the currently initialised WebSocket object. - - - `on_ticks(ws, ticks)` - Triggered when ticks are recevied. - - `ticks` - List of `tick` object. Check below for sample structure. - - `on_close(ws, code, reason)` - Triggered when connection is closed. - - `code` - WebSocket standard close event code (https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent) - - `reason` - DOMString indicating the reason the server closed the connection - - `on_error(ws, code, reason)` - Triggered when connection is closed with an error. - - `code` - WebSocket standard close event code (https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent) - - `reason` - DOMString indicating the reason the server closed the connection - - `on_connect` - Triggered when connection is established successfully. - - `response` - Response received from server on successful connection. - - `on_message(ws, payload, is_binary)` - Triggered when message is received from the server. - - `payload` - Raw response from the server (either text or binary). - - `is_binary` - Bool to check if response is binary type. - - `on_reconnect(ws, attempts_count)` - Triggered when auto reconnection is attempted. - - `attempts_count` - Current reconnect attempt number. - - `on_noreconnect(ws)` - Triggered when number of auto reconnection attempts exceeds `reconnect_tries`. - - `on_order_update(ws, data)` - Triggered when there is an order update for the connected user. - - - Tick structure (passed to the `on_ticks` callback) - --------------------------- - [{ - 'instrument_token': 53490439, - 'mode': 'full', - 'volume_traded': 12510, - 'last_price': 4084.0, - 'average_traded_price': 4086.55, - 'last_traded_quantity': 1, - 'total_buy_quantity': 2356 - 'total_sell_quantity': 2440, - 'change': 0.46740467404674046, - 'last_trade_time': datetime.datetime(2018, 1, 15, 13, 16, 54), - 'exchange_timestamp': datetime.datetime(2018, 1, 15, 13, 16, 56), - 'oi': 21845, - 'oi_day_low': 0, - 'oi_day_high': 0, - 'ohlc': { - 'high': 4093.0, - 'close': 4065.0, - 'open': 4088.0, - 'low': 4080.0 - }, - 'tradable': True, - 'depth': { - 'sell': [{ - 'price': 4085.0, - 'orders': 1048576, - 'quantity': 43 - }, { - 'price': 4086.0, - 'orders': 2752512, - 'quantity': 134 - }, { - 'price': 4087.0, - 'orders': 1703936, - 'quantity': 133 - }, { - 'price': 4088.0, - 'orders': 1376256, - 'quantity': 70 - }, { - 'price': 4089.0, - 'orders': 1048576, - 'quantity': 46 - }], - 'buy': [{ - 'price': 4084.0, - 'orders': 589824, - 'quantity': 53 - }, { - 'price': 4083.0, - 'orders': 1245184, - 'quantity': 145 - }, { - 'price': 4082.0, - 'orders': 1114112, - 'quantity': 63 - }, { - 'price': 4081.0, - 'orders': 1835008, - 'quantity': 69 - }, { - 'price': 4080.0, - 'orders': 2752512, - 'quantity': 89 - }] - } - }, - ..., - ...] - - Auto reconnection - ----------------- - - Auto reconnection is enabled by default and it can be disabled by passing `reconnect` param while initialising `KiteTicker`. - On a side note, reconnection mechanism cannot happen if event loop is terminated using `stop` method inside `on_close` callback. - - Auto reonnection mechanism is based on [Exponential backoff](https://en.wikipedia.org/wiki/Exponential_backoff) algorithm in which - next retry interval will be increased exponentially. `reconnect_max_delay` and `reconnect_max_tries` params can be used to tewak - the alogrithm where `reconnect_max_delay` is the maximum delay after which subsequent reconnection interval will become constant and - `reconnect_max_tries` is maximum number of retries before its quiting reconnection. - - For example if `reconnect_max_delay` is 60 seconds and `reconnect_max_tries` is 50 then the first reconnection interval starts from - minimum interval which is 2 seconds and keep increasing up to 60 seconds after which it becomes constant and when reconnection attempt - is reached upto 50 then it stops reconnecting. - - method `stop_retry` can be used to stop ongoing reconnect attempts and `on_reconnect` callback will be called with current reconnect - attempt and `on_noreconnect` is called when reconnection attempts reaches max retries. + # Connect and run + asyncio.run(kws.connect()) """ EXCHANGE_MAP = { @@ -366,366 +73,323 @@ def on_close(ws, code, reason): "mcx": 7, "mcxsx": 8, "indices": 9, - # bsecds is replaced with it's official segment name bcd - # so,bsecds key will be depreciated in next version - "bsecds": 6, + "bsecds": 6, # Deprecated, use bcd } - # Default connection timeout + # Connection settings CONNECT_TIMEOUT = 30 - # Default Reconnect max delay. + PING_INTERVAL = 2.5 + PING_TIMEOUT = 10 RECONNECT_MAX_DELAY = 60 - # Default reconnect attempts RECONNECT_MAX_TRIES = 50 - # Default root API endpoint. It's possible to - # override this by passing the `root` parameter during initialisation. ROOT_URI = "wss://ws.kite.trade" - # Available streaming modes. + # Streaming modes MODE_FULL = "full" MODE_QUOTE = "quote" MODE_LTP = "ltp" - # Flag to set if its first connect - _is_first_connect = True - - # Available actions. - _message_code = 11 + # Message constants _message_subscribe = "subscribe" _message_unsubscribe = "unsubscribe" _message_setmode = "mode" - # Minimum delay which should be set between retries. User can't set less than this - _minimum_reconnect_max_delay = 5 - # Maximum number or retries user can set - _maximum_reconnect_max_tries = 300 - - def __init__(self, api_key, access_token, debug=False, root=None, - reconnect=True, reconnect_max_tries=RECONNECT_MAX_TRIES, reconnect_max_delay=RECONNECT_MAX_DELAY, - connect_timeout=CONNECT_TIMEOUT): + def __init__( + self, + api_key: str, + access_token: str, + debug: bool = False, + root: Optional[str] = None, + reconnect: bool = True, + reconnect_max_tries: int = RECONNECT_MAX_TRIES, + reconnect_max_delay: int = RECONNECT_MAX_DELAY, + connect_timeout: int = CONNECT_TIMEOUT, + ping_interval: float = PING_INTERVAL, + ping_timeout: float = PING_TIMEOUT, + ): """ - Initialise websocket client instance. - - - `api_key` is the API key issued to you - - `access_token` is the token obtained after the login flow in - exchange for the `request_token`. Pre-login, this will default to None, - but once you have obtained it, you should - persist it in a database or session to pass - to the Kite Connect class initialisation for subsequent requests. - - `root` is the websocket API end point root. Unless you explicitly - want to send API requests to a non-default endpoint, this - can be ignored. - - `reconnect` is a boolean to enable WebSocket autreconnect in case of network failure/disconnection. - - `reconnect_max_delay` in seconds is the maximum delay after which subsequent reconnection interval will become constant. Defaults to 60s and minimum acceptable value is 5s. - - `reconnect_max_tries` is maximum number reconnection attempts. Defaults to 50 attempts and maximum up to 300 attempts. - - `connect_timeout` in seconds is the maximum interval after which connection is considered as timeout. Defaults to 30s. + Initialize WebSocket client. + + Args: + api_key: API key from Kite Connect + access_token: Access token from login flow + debug: Enable debug logging + root: WebSocket endpoint URL + reconnect: Enable auto-reconnection + reconnect_max_tries: Maximum reconnection attempts + reconnect_max_delay: Maximum delay between reconnections + connect_timeout: Connection timeout in seconds """ + self.api_key = api_key + self.access_token = access_token + self.debug = debug self.root = root or self.ROOT_URI - - # Set max reconnect tries - if reconnect_max_tries > self._maximum_reconnect_max_tries: - log.warning("`reconnect_max_tries` can not be more than {val}. Setting to highest possible value - {val}.".format( - val=self._maximum_reconnect_max_tries)) - self.reconnect_max_tries = self._maximum_reconnect_max_tries - else: - self.reconnect_max_tries = reconnect_max_tries - - # Set max reconnect delay - if reconnect_max_delay < self._minimum_reconnect_max_delay: - log.warning("`reconnect_max_delay` can not be less than {val}. Setting to lowest possible value - {val}.".format( - val=self._minimum_reconnect_max_delay)) - self.reconnect_max_delay = self._minimum_reconnect_max_delay - else: - self.reconnect_max_delay = reconnect_max_delay - + self.reconnect = reconnect + self.ping_interval = ping_interval + self.ping_timeout = ping_timeout + self.reconnect_max_tries = min(reconnect_max_tries, 300) + self.reconnect_max_delay = max(reconnect_max_delay, 5) self.connect_timeout = connect_timeout - self.socket_url = "{root}?api_key={api_key}"\ - "&access_token={access_token}".format( - root=self.root, - api_key=api_key, - access_token=access_token - ) - - # Debug enables logs - self.debug = debug - - # Initialize default value for websocket object - self.ws = None - - # Placeholders for callbacks. - self.on_ticks = None - self.on_open = None - self.on_close = None - self.on_error = None - self.on_connect = None - self.on_message = None - self.on_reconnect = None - self.on_noreconnect = None - - # Text message updates - self.on_order_update = None - - # List of current subscribed tokens - self.subscribed_tokens = {} - - def _create_connection(self, url, **kwargs): - """Create a WebSocket client connection.""" - self.factory = KiteTickerClientFactory(url, **kwargs) + # Connection state + self.ws: Optional[ClientConnection] = None + self._running = False + self._reconnect_attempts = 0 + self._last_pong = None + self.subscribed_tokens: Dict[int, str] = {} + + # Callbacks - can be sync or async + self.on_ticks: Optional[Callable] = None + self.on_open: Optional[Callable] = None + self.on_close: Optional[Callable] = None + self.on_error: Optional[Callable] = None + self.on_connect: Optional[Callable] = None + self.on_message: Optional[Callable] = None + self.on_reconnect: Optional[Callable] = None + self.on_noreconnect: Optional[Callable] = None + self.on_order_update: Optional[Callable] = None + + def _build_url(self) -> str: + """Build WebSocket URL with authentication.""" + params = {"api_key": self.api_key, "access_token": self.access_token} + return f"{self.root}?{urlencode(params)}" + + def _get_user_agent(self) -> str: + """Get user agent string.""" + return f"{__title__}-python/{__version__}" + + async def _call_callback(self, callback: Optional[Callable], *args, **kwargs): + """Call callback function (sync or async).""" + if callback: + try: + if iscoroutinefunction(callback): + await callback(*args, **kwargs) + else: + callback(*args, **kwargs) + except Exception as e: + log.error(f"Error in callback: {e}") + + async def connect(self) -> None: + """Connect to WebSocket and handle reconnections.""" + self._running = True + + while self._running: + try: + await self._connect_once() + if not self.reconnect or not self._running: + break + + # Calculate reconnect delay with exponential backoff + delay = min(2**self._reconnect_attempts, self.reconnect_max_delay) + log.info( + f"Reconnecting in {delay} seconds... (attempt {self._reconnect_attempts + 1})" + ) + + await self._call_callback( + self.on_reconnect, self._reconnect_attempts + 1 + ) + await sleep(delay) + + self._reconnect_attempts += 1 + + if self._reconnect_attempts >= self.reconnect_max_tries: + log.error( + f"Maximum reconnection attempts ({self.reconnect_max_tries}) reached" + ) + await self._call_callback(self.on_noreconnect) + break + + except KeyboardInterrupt: + log.info("Connection interrupted by user") + break + except Exception as e: + log.error(f"Unexpected error: {e}") + break + + async def _connect_once(self) -> None: + """Single connection attempt.""" + url = self._build_url() + headers = {"User-Agent": self._get_user_agent(), "X-Kite-Version": "3"} + + # SSL context + ssl_context = create_default_context() - # Alias for current websocket connection - self.ws = self.factory.ws - - self.factory.debug = self.debug - - # Register private callbacks - self.factory.on_open = self._on_open - self.factory.on_error = self._on_error - self.factory.on_close = self._on_close - self.factory.on_message = self._on_message - self.factory.on_connect = self._on_connect - self.factory.on_reconnect = self._on_reconnect - self.factory.on_noreconnect = self._on_noreconnect - - self.factory.maxDelay = self.reconnect_max_delay - self.factory.maxRetries = self.reconnect_max_tries + try: + async with connect( + url, + extra_headers=headers, + ssl=ssl_context, + ping_interval=self.PING_INTERVAL, + ping_timeout=self.PING_TIMEOUT, + close_timeout=self.connect_timeout, + max_size=None, + ) as websocket: + self.ws = websocket + log.info("WebSocket connected successfully") + + # Reset reconnect counter on successful connection + self._reconnect_attempts = 0 + + await self._call_callback(self.on_connect, self) + await self._call_callback(self.on_open, self) + + # Resubscribe to existing tokens + if self.subscribed_tokens: + await self._resubscribe() + + # Handle messages + async for message in websocket: + await self._handle_message(message) + + except ConnectionClosed as e: + log.warning(f"WebSocket connection closed: {e}") + await self._call_callback(self.on_close, self, e.code, e.reason) - def _user_agent(self): - return (__title__ + "-python/").capitalize() + __version__ + except Exception as e: + log.error(f"WebSocket connection error: {e}") + await self._call_callback(self.on_error, self, 0, str(e)) - def connect(self, threaded=False, disable_ssl_verification=False, proxy=None): - """ - Establish a websocket connection. + async def _handle_message(self, message) -> None: + """Handle incoming WebSocket message.""" + try: + await self._call_callback( + self.on_message, self, message, isinstance(message, bytes) + ) - - `threaded` is a boolean indicating if the websocket client has to be run in threaded mode or not - - `disable_ssl_verification` disables building ssl context - - `proxy` is a dictionary with keys `host` and `port` which denotes the proxy settings - """ - # Custom headers - headers = { - "X-Kite-Version": "3", # For version 3 - } - - # Init WebSocket client factory - self._create_connection(self.socket_url, - useragent=self._user_agent(), - proxy=proxy, headers=headers) - - # Set SSL context - context_factory = None - if self.factory.isSecure and not disable_ssl_verification: - context_factory = ssl.ClientContextFactory() - - # Establish WebSocket connection to a server - connectWS(self.factory, contextFactory=context_factory, timeout=self.connect_timeout) - - if self.debug: - twisted_log.startLogging(sys.stdout) - - # Run in seperate thread of blocking - opts = {} - - # Run when reactor is not running - if not reactor.running: - if threaded: - # Signals are not allowed in non main thread by twisted so suppress it. - opts["installSignalHandlers"] = False - self.websocket_thread = threading.Thread(target=reactor.run, kwargs=opts) - self.websocket_thread.daemon = True - self.websocket_thread.start() + if isinstance(message, bytes): + # Binary market data + if len(message) > 4 and self.on_ticks: + ticks = self._parse_binary(message) + await self._call_callback(self.on_ticks, self, ticks) else: - reactor.run(**opts) - - def is_connected(self): - """Check if WebSocket connection is established.""" - if self.ws and self.ws.state == self.ws.STATE_OPEN: - return True - else: - return False - - def _close(self, code=None, reason=None): - """Close the WebSocket connection.""" - if self.ws: - self.ws.sendClose(code, reason) + # Text message + await self._parse_text_message(message) - def close(self, code=None, reason=None): - """Close the WebSocket connection.""" - self.stop_retry() - self._close(code, reason) - - def stop(self): - """Stop the event loop. Should be used if main thread has to be closed in `on_close` method. - Reconnection mechanism cannot happen past this method - """ - reactor.stop() + except Exception as e: + log.error(f"Error handling message: {e}") - def stop_retry(self): - """Stop auto retry when it is in progress.""" - if self.factory: - self.factory.stopTrying() + async def _parse_text_message(self, message: str) -> None: + """Parse text message from WebSocket.""" + try: + data = loads(message) + + # Handle order updates + if ( + data.get("type") == "order" + and data.get("data") + and self.on_order_update + ): + await self._call_callback(self.on_order_update, self, data["data"]) + + # Handle errors + elif data.get("type") == "error": + await self._call_callback(self.on_error, self, 0, data.get("data")) + + except JSONDecodeError as e: + log.error(f"Failed to parse text message: {e}") + + async def _resubscribe(self) -> None: + """Resubscribe to all previously subscribed tokens.""" + if not self.subscribed_tokens: + return - def subscribe(self, instrument_tokens): - """ - Subscribe to a list of instrument_tokens. + # Group tokens by mode + modes: Dict[str, List[int]] = {} + for token, mode in self.subscribed_tokens.items(): + modes.setdefault(mode, []).append(token) + + # Subscribe and set modes + for mode, tokens in modes.items(): + if mode not in ["full", "quote", "ltp"]: + log.warning(f"Unknown mode: {mode}") + continue + log.debug(f"Resubscribing to {len(tokens)} tokens in {mode} mode") + await self.subscribe(tokens) + await self.set_mode(mode, tokens) # pyright: ignore[reportArgumentType] + + async def subscribe(self, instrument_tokens: List[int]) -> bool: + """Subscribe to instrument tokens.""" + if self.ws is None: + return False - - `instrument_tokens` is list of instrument instrument_tokens to subscribe - """ try: - self.ws.sendMessage( - six.b(json.dumps({"a": self._message_subscribe, "v": instrument_tokens})) - ) + message = dumps({"a": self._message_subscribe, "v": instrument_tokens}) + await self.ws.send(message) + # Update subscribed tokens for token in instrument_tokens: self.subscribed_tokens[token] = self.MODE_QUOTE return True + except Exception as e: - self._close(reason="Error while subscribe: {}".format(str(e))) - raise + log.error(f"Error subscribing: {e}") + await self.close() + return False - def unsubscribe(self, instrument_tokens): - """ - Unsubscribe the given list of instrument_tokens. + async def unsubscribe(self, instrument_tokens: List[int]) -> bool: + """Unsubscribe from instrument tokens.""" + if self.ws is None: + return False - - `instrument_tokens` is list of instrument_tokens to unsubscribe. - """ try: - self.ws.sendMessage( - six.b(json.dumps({"a": self._message_unsubscribe, "v": instrument_tokens})) - ) + message = dumps({"a": self._message_unsubscribe, "v": instrument_tokens}) + await self.ws.send(message) + # Remove from subscribed tokens for token in instrument_tokens: - try: - del (self.subscribed_tokens[token]) - except KeyError: - pass + self.subscribed_tokens.pop(token, None) return True + except Exception as e: - self._close(reason="Error while unsubscribe: {}".format(str(e))) - raise + log.error(f"Error unsubscribing: {e}") + await self.close() + return False - def set_mode(self, mode, instrument_tokens): - """ - Set streaming mode for the given list of tokens. + async def set_mode( + self, mode: Literal["full", "quote", "ltp"], instrument_tokens: List[int] + ) -> bool: + """Set streaming mode for instrument tokens.""" + if self.ws is None: + return False - - `mode` is the mode to set. It can be one of the following class constants: - MODE_LTP, MODE_QUOTE, or MODE_FULL. - - `instrument_tokens` is list of instrument tokens on which the mode should be applied - """ try: - self.ws.sendMessage( - six.b(json.dumps({"a": self._message_setmode, "v": [mode, instrument_tokens]})) + message = dumps( + {"a": self._message_setmode, "v": [mode, instrument_tokens]} ) + await self.ws.send(message) # Update modes for token in instrument_tokens: self.subscribed_tokens[token] = mode return True - except Exception as e: - self._close(reason="Error while setting mode: {}".format(str(e))) - raise - - def resubscribe(self): - """Resubscribe to all current subscribed tokens.""" - modes = {} - - for token in self.subscribed_tokens: - m = self.subscribed_tokens[token] - - if not modes.get(m): - modes[m] = [] - - modes[m].append(token) - - for mode in modes: - if self.debug: - log.debug("Resubscribe and set mode: {} - {}".format(mode, modes[mode])) - - self.subscribe(modes[mode]) - self.set_mode(mode, modes[mode]) - - def _on_connect(self, ws, response): - self.ws = ws - if self.on_connect: - self.on_connect(self, response) - - def _on_close(self, ws, code, reason): - """Call `on_close` callback when connection is closed.""" - log.error("Connection closed: {} - {}".format(code, str(reason))) - if self.on_close: - self.on_close(self, code, reason) - - def _on_error(self, ws, code, reason): - """Call `on_error` callback when connection throws an error.""" - log.error("Connection error: {} - {}".format(code, str(reason))) - - if self.on_error: - self.on_error(self, code, reason) - - def _on_message(self, ws, payload, is_binary): - """Call `on_message` callback when text message is received.""" - if self.on_message: - self.on_message(self, payload, is_binary) - - # If the message is binary, parse it and send it to the callback. - if self.on_ticks and is_binary and len(payload) > 4: - self.on_ticks(self, self._parse_binary(payload)) - - # Parse text messages - if not is_binary: - self._parse_text_message(payload) - - def _on_open(self, ws): - # Resubscribe if its reconnect - if not self._is_first_connect: - self.resubscribe() - - # Set first connect to false once its connected first time - self._is_first_connect = False - - if self.on_open: - return self.on_open(self) - - def _on_reconnect(self, attempts_count): - if self.on_reconnect: - return self.on_reconnect(self, attempts_count) - - def _on_noreconnect(self): - if self.on_noreconnect: - return self.on_noreconnect(self) - - def _parse_text_message(self, payload): - """Parse text message.""" - # Decode unicode data - if not six.PY2 and type(payload) == bytes: - payload = payload.decode("utf-8") - - try: - data = json.loads(payload) - except ValueError: - return - - # Order update callback - if self.on_order_update and data.get("type") == "order" and data.get("data"): - self.on_order_update(self, data["data"]) + except Exception as e: + log.error(f"Error setting mode: {e}") + await self.close() + return False - # Custom error with websocket error code 0 - if data.get("type") == "error": - self._on_error(self, 0, data.get("data")) + async def close(self) -> None: + """Close WebSocket connection.""" + self._running = False + if self.ws: + await self.ws.close() - def _parse_binary(self, bin): - """Parse binary data to a (list of) ticks structure.""" - packets = self._split_packets(bin) # split data to individual ticks packet - data = [] + def _parse_binary(self, data: bytes) -> List[Dict[str, Any]]: + """Parse binary market data into tick structures.""" + packets = self._split_packets(data) + ticks = [] for packet in packets: + if len(packet) < 4: + continue + instrument_token = self._unpack_int(packet, 0, 4) - segment = instrument_token & 0xff # Retrive segment constant from instrument_token + segment = instrument_token & 0xFF - # Add price divisor based on segment + # Price divisor based on segment if segment == self.EXCHANGE_MAP["cds"]: divisor = 10000000.0 elif segment == self.EXCHANGE_MAP["bcd"]: @@ -733,131 +397,158 @@ def _parse_binary(self, bin): else: divisor = 100.0 - # All indices are not tradable - tradable = False if segment == self.EXCHANGE_MAP["indices"] else True + tradable = segment != self.EXCHANGE_MAP["indices"] + tick = {"tradable": tradable, "instrument_token": instrument_token} - # LTP packets + # Parse different packet sizes if len(packet) == 8: - data.append({ - "tradable": tradable, - "mode": self.MODE_LTP, - "instrument_token": instrument_token, - "last_price": self._unpack_int(packet, 4, 8) / divisor - }) - # Indices quote and full mode - elif len(packet) == 28 or len(packet) == 32: - mode = self.MODE_QUOTE if len(packet) == 28 else self.MODE_FULL - - d = { - "tradable": tradable, - "mode": mode, - "instrument_token": instrument_token, - "last_price": self._unpack_int(packet, 4, 8) / divisor, - "ohlc": { - "high": self._unpack_int(packet, 8, 12) / divisor, - "low": self._unpack_int(packet, 12, 16) / divisor, - "open": self._unpack_int(packet, 16, 20) / divisor, - "close": self._unpack_int(packet, 20, 24) / divisor + # LTP mode + tick.update( + { + "mode": self.MODE_LTP, + "last_price": self._unpack_int(packet, 4, 8) / divisor, } - } + ) + + elif len(packet) in (28, 32): + # Index quote/full mode + tick.update( + { + "mode": self.MODE_FULL + if len(packet) == 32 + else self.MODE_QUOTE, + "last_price": self._unpack_int(packet, 4, 8) / divisor, + "ohlc": { + "high": self._unpack_int(packet, 8, 12) / divisor, + "low": self._unpack_int(packet, 12, 16) / divisor, + "open": self._unpack_int(packet, 16, 20) / divisor, + "close": self._unpack_int(packet, 20, 24) / divisor, + }, + } + ) - # Compute the change price using close price and last price - d["change"] = 0 - if (d["ohlc"]["close"] != 0): - d["change"] = (d["last_price"] - d["ohlc"]["close"]) * 100 / d["ohlc"]["close"] + # Calculate change percentage + close = tick["ohlc"]["close"] + if close != 0: + tick["change"] = (tick["last_price"] - close) * 100 / close + else: + tick["change"] = 0 - # Full mode with timestamp + # Full mode timestamp if len(packet) == 32: try: - timestamp = datetime.fromtimestamp(self._unpack_int(packet, 28, 32)) - except Exception: - timestamp = None - - d["exchange_timestamp"] = timestamp - - data.append(d) - # Quote and full mode - elif len(packet) == 44 or len(packet) == 184: - mode = self.MODE_QUOTE if len(packet) == 44 else self.MODE_FULL - - d = { - "tradable": tradable, - "mode": mode, - "instrument_token": instrument_token, - "last_price": self._unpack_int(packet, 4, 8) / divisor, - "last_traded_quantity": self._unpack_int(packet, 8, 12), - "average_traded_price": self._unpack_int(packet, 12, 16) / divisor, - "volume_traded": self._unpack_int(packet, 16, 20), - "total_buy_quantity": self._unpack_int(packet, 20, 24), - "total_sell_quantity": self._unpack_int(packet, 24, 28), - "ohlc": { - "open": self._unpack_int(packet, 28, 32) / divisor, - "high": self._unpack_int(packet, 32, 36) / divisor, - "low": self._unpack_int(packet, 36, 40) / divisor, - "close": self._unpack_int(packet, 40, 44) / divisor + tick["exchange_timestamp"] = datetime.fromtimestamp( + self._unpack_int(packet, 28, 32) + ) + except (ValueError, OSError): + tick["exchange_timestamp"] = None + + elif len(packet) in (44, 184): + # Regular quote/full mode + tick.update( + { + "mode": self.MODE_FULL + if len(packet) == 184 + else self.MODE_QUOTE, + "last_price": self._unpack_int(packet, 4, 8) / divisor, + "last_traded_quantity": self._unpack_int(packet, 8, 12), + "average_traded_price": self._unpack_int(packet, 12, 16) + / divisor, + "volume_traded": self._unpack_int(packet, 16, 20), + "total_buy_quantity": self._unpack_int(packet, 20, 24), + "total_sell_quantity": self._unpack_int(packet, 24, 28), + "ohlc": { + "open": self._unpack_int(packet, 28, 32) / divisor, + "high": self._unpack_int(packet, 32, 36) / divisor, + "low": self._unpack_int(packet, 36, 40) / divisor, + "close": self._unpack_int(packet, 40, 44) / divisor, + }, } - } + ) - # Compute the change price using close price and last price - d["change"] = 0 - if (d["ohlc"]["close"] != 0): - d["change"] = (d["last_price"] - d["ohlc"]["close"]) * 100 / d["ohlc"]["close"] + # Calculate change percentage + close = tick["ohlc"]["close"] + if close != 0: + tick["change"] = (tick["last_price"] - close) * 100 / close + else: + tick["change"] = 0 - # Parse full mode + # Full mode additional data if len(packet) == 184: try: - last_trade_time = datetime.fromtimestamp(self._unpack_int(packet, 44, 48)) - except Exception: - last_trade_time = None + tick["last_trade_time"] = datetime.fromtimestamp( + self._unpack_int(packet, 44, 48) + ) + except (ValueError, OSError): + tick["last_trade_time"] = None + + tick.update( + { + "oi": self._unpack_int(packet, 48, 52), + "oi_day_high": self._unpack_int(packet, 52, 56), + "oi_day_low": self._unpack_int(packet, 56, 60), + } + ) try: - timestamp = datetime.fromtimestamp(self._unpack_int(packet, 60, 64)) - except Exception: - timestamp = None - - d["last_trade_time"] = last_trade_time - d["oi"] = self._unpack_int(packet, 48, 52) - d["oi_day_high"] = self._unpack_int(packet, 52, 56) - d["oi_day_low"] = self._unpack_int(packet, 56, 60) - d["exchange_timestamp"] = timestamp - - # Market depth entries. - depth = { - "buy": [], - "sell": [] - } - - # Compile the market depth lists. - for i, p in enumerate(range(64, len(packet), 12)): - depth["sell" if i >= 5 else "buy"].append({ - "quantity": self._unpack_int(packet, p, p + 4), - "price": self._unpack_int(packet, p + 4, p + 8) / divisor, - "orders": self._unpack_int(packet, p + 8, p + 10, byte_format="H") - }) + tick["exchange_timestamp"] = datetime.fromtimestamp( + self._unpack_int(packet, 60, 64) + ) + except (ValueError, OSError): + tick["exchange_timestamp"] = None + + # Market depth + depth = {"buy": [], "sell": []} + for i, pos in enumerate(range(64, len(packet), 12)): + if pos + 12 > len(packet): + break + + entry = { + "quantity": self._unpack_int(packet, pos, pos + 4), + "price": self._unpack_int(packet, pos + 4, pos + 8) + / divisor, + "orders": self._unpack_int(packet, pos + 8, pos + 10, "H"), + } + + if i < 5: + depth["buy"].append(entry) + else: + depth["sell"].append(entry) + + tick["depth"] = depth + + ticks.append(tick) + + return ticks + + def _split_packets(self, data: bytes) -> List[bytes]: + """Split binary data into individual tick packets.""" + if len(data) < 2: + return [] - d["depth"] = depth + packets = [] + packet_count = self._unpack_int(data, 0, 2, "H") + pos = 2 - data.append(d) + for _ in range(packet_count): + if pos + 2 > len(data): + break - return data + packet_length = self._unpack_int(data, pos, pos + 2, "H") + pos += 2 - def _unpack_int(self, bin, start, end, byte_format="I"): - """Unpack binary data as unsgined interger.""" - return struct.unpack(">" + byte_format, bin[start:end])[0] + if pos + packet_length > len(data): + break - def _split_packets(self, bin): - """Split the data to individual packets of ticks.""" - # Ignore heartbeat data. - if len(bin) < 2: - return [] - - number_of_packets = self._unpack_int(bin, 0, 2, byte_format="H") - packets = [] - - j = 2 - for i in range(number_of_packets): - packet_length = self._unpack_int(bin, j, j + 2, byte_format="H") - packets.append(bin[j + 2: j + 2 + packet_length]) - j = j + 2 + packet_length + packets.append(data[pos : pos + packet_length]) + pos += packet_length return packets + + def _unpack_int( + self, data: bytes, start: int, end: int, byte_format: Literal["H", "I"] = "I" + ) -> int: + """Unpack binary data as unsigned integer.""" + if end > len(data): + return 0 + return unpack(">" + byte_format, data[start:end])[0] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..4f3b350b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,39 @@ +[project] +name = "pykiteconnect" +version = "5.0.1" +description = "The official Python client for the Kite Connect trading API" +readme = "README.md" +requires-python = ">=3.10" +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Intended Audience :: Financial and Insurance Industry", + "Programming Language :: Python", + "Natural Language :: English", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Topic :: Office/Business :: Financial :: Investment", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Software Development :: Libraries" +] +dependencies = [ + "pyopenssl>=25.1.0", + "python-dateutil>=2.9.0.post0", + "requests>=2.32.5", + "service-identity>=18.1.0", + "six>=1.17.0", + "websockets>=15.0.1", +] + + +[dependency-groups] +dev = [ + "mock>=5.2.0", + "pytest>=8.4.1", + "pytest-cov>=6.2.1", + "responses>=0.25.8", + "ruff>=0.12.10", + "urllib3>=2.5.0", +] diff --git a/setup.py b/setup.py deleted file mode 100644 index 7518f3c0..00000000 --- a/setup.py +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env python - -import io -import os -from codecs import open -from setuptools import setup - -current_dir = os.path.abspath(os.path.dirname(__file__)) - -about = {} -with open(os.path.join(current_dir, "kiteconnect", "__version__.py"), "r", "utf-8") as f: - exec(f.read(), about) - -with io.open('README.md', 'rt', encoding='utf8') as f: - readme = f.read() - -setup( - name=about["__title__"], - version=about["__version__"], - description=about["__description__"], - long_description=readme, - long_description_content_type='text/markdown', - author=about["__author__"], - author_email=about["__author_email__"], - url=about["__url__"], - download_url=about["__download_url__"], - license=about["__license__"], - packages=["kiteconnect"], - classifiers=[ - "Development Status :: 5 - Production/Stable", - "Intended Audience :: Developers", - "Intended Audience :: Financial and Insurance Industry", - "Programming Language :: Python", - "Natural Language :: English", - "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3.5", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Topic :: Office/Business :: Financial :: Investment", - "Topic :: Software Development :: Libraries :: Python Modules", - "Topic :: Software Development :: Libraries" - ], - install_requires=[ - "service_identity>=18.1.0", - "requests>=2.18.4", - "six>=1.11.0", - "pyOpenSSL>=17.5.0", - "python-dateutil>=2.6.1", - "autobahn[twisted]==19.11.2" - ], - tests_require=["pytest", "responses", "pytest-cov", "mock", "flake8"], - test_suite="tests", - setup_requires=["pytest-runner"], - extras_require={ - "doc": ["pdoc"], - ':sys_platform=="win32"': ["pywin32"] - } -) diff --git a/tests/helpers/utils.py b/tests/helpers/utils.py index f58d7d28..65988788 100644 --- a/tests/helpers/utils.py +++ b/tests/helpers/utils.py @@ -8,56 +8,42 @@ "user.profile": "profile.json", "user.margins": "margins.json", "user.margins.segment": "margins.json", - "orders": "orders.json", "trades": "trades.json", "order.info": "order_info.json", "order.trades": "order_trades.json", - "portfolio.positions": "positions.json", "portfolio.holdings": "holdings.json", "portfolio.holdings.auction": "auctions_list.json", - # MF api endpoints "mf.orders": "mf_orders.json", "mf.order.info": "mf_orders_info.json", - "mf.sips": "mf_sips.json", "mf.sip.info": "mf_sip_info.json", - "mf.holdings": "mf_holdings.json", "mf.instruments": "mf_instruments.csv", - "market.instruments": "instruments_nse.csv", "market.instruments.all": "instruments_all.csv", "market.historical": "historical_minute.json", "market.trigger_range": "trigger_range.json", - "market.quote": "quote.json", "market.quote.ohlc": "ohlc.json", "market.quote.ltp": "ltp.json", - "gtt": "gtt_get_orders.json", "gtt.place": "gtt_place_order.json", "gtt.info": "gtt_get_order.json", "gtt.modify": "gtt_modify_order.json", "gtt.delete": "gtt_delete_order.json", - # Order margin & charges "order.margins": "order_margins.json", "order.margins.basket": "basket_margins.json", - "order.contract_note": "virtual_contract_note.json" + "order.contract_note": "virtual_contract_note.json", } def full_path(rel_path): """return the full path of given rel_path.""" - return os.path.abspath( - os.path.join( - os.path.dirname(__file__), - rel_path - ) - ) + return os.path.abspath(os.path.join(os.path.dirname(__file__), rel_path)) def get_response(key): diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 8912640f..0d69203b 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -1,12 +1,13 @@ # coding: utf-8 """Pytest config.""" + import os import sys import pytest from kiteconnect import KiteConnect -sys.path.append(os.path.join(os.path.dirname(__file__), '../helpers')) +sys.path.append(os.path.join(os.path.dirname(__file__), "../helpers")) def pytest_addoption(parser): @@ -35,4 +36,6 @@ def pytest_generate_tests(metafunc): @pytest.fixture() def kiteconnect(api_key, access_token, root_url): """Init Kite connect object.""" - return KiteConnect(api_key=api_key, access_token=access_token, root=root_url or None) + return KiteConnect( + api_key=api_key, access_token=access_token, root=root_url or None + ) diff --git a/tests/integration/test_connect_read.py b/tests/integration/test_connect_read.py index 8724c9e8..9c221a11 100644 --- a/tests/integration/test_connect_read.py +++ b/tests/integration/test_connect_read.py @@ -11,11 +11,12 @@ def test_request_pool(): from kiteconnect import KiteConnect + pool = { "pool_connections": 10, "pool_maxsize": 10, "max_retries": 0, - "pool_block": False + "pool_block": False, } kiteconnect = KiteConnect(api_key="random", access_token="random", pool=pool) @@ -60,7 +61,7 @@ def test_holdings(kiteconnect): def test_auction_instruments(kiteconnect): - """ Test get_auction_instruments """ + """Test get_auction_instruments""" auction_inst = kiteconnect.get_auction_instruments() mock_resp = utils.get_json_response("portfolio.holdings.auction")["data"] utils.assert_responses(auction_inst, mock_resp) @@ -90,7 +91,11 @@ def test_order_history(kiteconnect): orders = kiteconnect.orders() if len(orders) == 0: - warnings.warn(UserWarning("Order info: Couldn't perform individual order test since orderbook is empty.")) + warnings.warn( + UserWarning( + "Order info: Couldn't perform individual order test since orderbook is empty." + ) + ) return order = kiteconnect.order_history(order_id=orders[0]["order_id"]) @@ -119,7 +124,11 @@ def test_order_trades(kiteconnect): trades = kiteconnect.trades() if len(trades) == 0: - warnings.warn(UserWarning("Trades: Couldn't perform individual order test since trades is empty.")) + warnings.warn( + UserWarning( + "Trades: Couldn't perform individual order test since trades is empty." + ) + ) return order_trades = kiteconnect.order_trades(order_id=trades[0]["order_id"]) @@ -154,7 +163,11 @@ def test_mf_order_info(kiteconnect): orders = kiteconnect.mf_orders() if len(orders) == 0: - warnings.warn(UserWarning("MF order info: Couldn't perform individual order test since orderbook is empty.")) + warnings.warn( + UserWarning( + "MF order info: Couldn't perform individual order test since orderbook is empty." + ) + ) return order = kiteconnect.mf_orders(order_id=orders[0]["order_id"]) @@ -187,27 +200,32 @@ def test_mf_instruments(kiteconnect): # Historical API tests ###################### -@pytest.mark.parametrize("max_interval,candle_interval", [ - (30, "minute"), - (365, "hour"), - (2000, "day"), - (90, "3minute"), - (90, "5minute"), - (90, "10minute"), - (180, "15minute"), - (180, "30minute"), - (365, "60minute") -], ids=[ - "minute", - "hour", - "day", - "3minute", - "5minute", - "10minute", - "15minute", - "30minute", - "60minute", -]) + +@pytest.mark.parametrize( + "max_interval,candle_interval", + [ + (30, "minute"), + (365, "hour"), + (2000, "day"), + (90, "3minute"), + (90, "5minute"), + (90, "10minute"), + (180, "15minute"), + (180, "30minute"), + (365, "60minute"), + ], + ids=[ + "minute", + "hour", + "day", + "3minute", + "5minute", + "10minute", + "15minute", + "30minute", + "60minute", + ], +) def test_historical_data_intervals(max_interval, candle_interval, kiteconnect): """Test historical data for each intervals""" # Reliance token @@ -215,17 +233,23 @@ def test_historical_data_intervals(max_interval, candle_interval, kiteconnect): to_date = datetime.datetime.now() diff = int(max_interval / 3) - from_date = (to_date - datetime.timedelta(days=diff)) + from_date = to_date - datetime.timedelta(days=diff) # minute data - data = kiteconnect.historical_data(instrument_token, from_date, to_date, candle_interval) - mock_resp = kiteconnect._format_historical(utils.get_json_response("market.historical")["data"]) + data = kiteconnect.historical_data( + instrument_token, from_date, to_date, candle_interval + ) + mock_resp = kiteconnect._format_historical( + utils.get_json_response("market.historical")["data"] + ) utils.assert_responses(data, mock_resp) # Max interval - from_date = (to_date - datetime.timedelta(days=(max_interval + 1))) + from_date = to_date - datetime.timedelta(days=(max_interval + 1)) with pytest.raises(ex.InputException): - kiteconnect.historical_data(instrument_token, from_date, to_date, candle_interval) + kiteconnect.historical_data( + instrument_token, from_date, to_date, candle_interval + ) def test_quote(kiteconnect): @@ -288,7 +312,9 @@ def test_trigger_range(kiteconnect): mock_resp = utils.get_json_response("market.trigger_range")["data"] utils.assert_responses(buy_resp, mock_resp) - buy_resp = kiteconnect.trigger_range(kiteconnect.TRANSACTION_TYPE_SELL, *instruments) + buy_resp = kiteconnect.trigger_range( + kiteconnect.TRANSACTION_TYPE_SELL, *instruments + ) mock_resp = utils.get_json_response("market.trigger_range")["data"] utils.assert_responses(buy_resp, mock_resp) diff --git a/tests/integration/test_connect_write.py b/tests/integration/test_connect_write.py index 56dfbe79..394f8c33 100644 --- a/tests/integration/test_connect_write.py +++ b/tests/integration/test_connect_write.py @@ -10,36 +10,36 @@ "exchange": "NSE", "tradingsymbol": "RELIANCE", "transaction_type": "BUY", - "quantity": 1 + "quantity": 1, } def is_pending_order(status): """Check if the status is pending order status.""" status = status.upper() - if ("COMPLETE" in status or "REJECT" in status or "CANCEL" in status): + if "COMPLETE" in status or "REJECT" in status or "CANCEL" in status: return False return True -def setup_order_place(kiteconnect, - variety, - product, - order_type, - diff_constant=0.01, - price_diff=1, - price=None, - validity=None, - disclosed_quantity=None, - trigger_price=None, - tag="itest"): +def setup_order_place( + kiteconnect, + variety, + product, + order_type, + diff_constant=0.01, + price_diff=1, + price=None, + validity=None, + disclosed_quantity=None, + trigger_price=None, + tag="itest", +): """Place an order with custom fields enabled. Prices are calculated from live ltp and offset based on `price_diff` and `diff_constant.`""" - updated_params = utils.merge_dicts(params, { - "product": product, - "variety": variety, - "order_type": order_type - }) + updated_params = utils.merge_dicts( + params, {"product": product, "variety": variety, "order_type": order_type} + ) # NOT WORKING CURRENTLY # Raises exception since no price set @@ -84,17 +84,20 @@ def cleanup_orders(kiteconnect, order_id=None): # Cancel order if order is open if is_pending_order(status): - kiteconnect.cancel_order(variety=variety, order_id=order_id, parent_order_id=parent_order_id) + kiteconnect.cancel_order( + variety=variety, order_id=order_id, parent_order_id=parent_order_id + ) # If complete then fetch positions and exit elif "COMPLETE" in status: positions = kiteconnect.positions() for p in positions["net"]: - if (p["tradingsymbol"] == tradingsymbol and - p["exchange"] == exchange and - p["product"] == product and - p["quantity"] != 0 and - p["product"] not in [kiteconnect.PRODUCT_BO, kiteconnect.PRODUCT_CO]): - + if ( + p["tradingsymbol"] == tradingsymbol + and p["exchange"] == exchange + and p["product"] == product + and p["quantity"] != 0 + and p["product"] not in [kiteconnect.PRODUCT_BO, kiteconnect.PRODUCT_CO] + ): updated_params = { "tradingsymbol": p["tradingsymbol"], "exchange": p["exchange"], @@ -102,13 +105,16 @@ def cleanup_orders(kiteconnect, order_id=None): "quantity": abs(p["quantity"]), "product": p["product"], "variety": kiteconnect.VARIETY_REGULAR, - "order_type": kiteconnect.ORDER_TYPE_MARKET + "order_type": kiteconnect.ORDER_TYPE_MARKET, } kiteconnect.place_order(**updated_params) # If order is complete and CO/BO order then exit the orde - if "COMPLETE" in status and variety in [kiteconnect.VARIETY_BO, kiteconnect.VARIETY_CO]: + if "COMPLETE" in status and variety in [ + kiteconnect.VARIETY_BO, + kiteconnect.VARIETY_CO, + ]: orders = kiteconnect.orders() leg_order_id = None for o in orders: @@ -117,19 +123,22 @@ def cleanup_orders(kiteconnect, order_id=None): break if leg_order_id: - kiteconnect.exit_order(variety=variety, order_id=leg_order_id, parent_order_id=order_id) + kiteconnect.exit_order( + variety=variety, order_id=leg_order_id, parent_order_id=order_id + ) # Order place tests ##################### + def test_place_order_market_regular(kiteconnect): """Place regular marker order.""" updated_params, order_id, order = setup_order_place( kiteconnect=kiteconnect, product=kiteconnect.PRODUCT_MIS, variety=kiteconnect.VARIETY_REGULAR, - order_type=kiteconnect.ORDER_TYPE_MARKET + order_type=kiteconnect.ORDER_TYPE_MARKET, ) assert order[-1]["product"] == kiteconnect.PRODUCT_MIS @@ -148,7 +157,7 @@ def test_place_order_limit_regular(kiteconnect): product=kiteconnect.PRODUCT_MIS, variety=kiteconnect.VARIETY_REGULAR, order_type=kiteconnect.ORDER_TYPE_LIMIT, - price=True + price=True, ) assert order[-1]["product"] == kiteconnect.PRODUCT_MIS @@ -168,7 +177,7 @@ def test_place_order_sl_regular(kiteconnect): variety=kiteconnect.VARIETY_REGULAR, order_type=kiteconnect.ORDER_TYPE_SL, price=True, - trigger_price=True + trigger_price=True, ) assert order[-1]["product"] == kiteconnect.PRODUCT_MIS @@ -189,7 +198,7 @@ def test_place_order_slm_regular(kiteconnect): product=kiteconnect.PRODUCT_MIS, variety=kiteconnect.VARIETY_REGULAR, order_type=kiteconnect.ORDER_TYPE_SLM, - trigger_price=True + trigger_price=True, ) assert order[-1]["trigger_price"] @@ -206,12 +215,15 @@ def test_place_order_slm_regular(kiteconnect): def test_place_order_tag(kiteconnect): """Send custom tag and get it in orders.""" tag = "mytag" - updated_params = utils.merge_dicts(params, { - "product": kiteconnect.PRODUCT_MIS, - "variety": kiteconnect.VARIETY_REGULAR, - "order_type": kiteconnect.ORDER_TYPE_MARKET, - "tag": tag - }) + updated_params = utils.merge_dicts( + params, + { + "product": kiteconnect.PRODUCT_MIS, + "variety": kiteconnect.VARIETY_REGULAR, + "order_type": kiteconnect.ORDER_TYPE_MARKET, + "tag": tag, + }, + ) order_id = kiteconnect.place_order(**updated_params) order_info = kiteconnect.order_history(order_id=order_id) @@ -230,7 +242,7 @@ def test_place_order_co_market(kiteconnect): product=kiteconnect.PRODUCT_MIS, variety=kiteconnect.VARIETY_CO, order_type=kiteconnect.ORDER_TYPE_MARKET, - trigger_price=True + trigger_price=True, ) assert order[-1]["product"] == kiteconnect.PRODUCT_CO @@ -249,7 +261,7 @@ def test_place_order_co_limit(kiteconnect): product=kiteconnect.PRODUCT_MIS, variety=kiteconnect.VARIETY_CO, order_type=kiteconnect.ORDER_TYPE_LIMIT, - trigger_price=True + trigger_price=True, ) assert order[-1]["product"] == kiteconnect.PRODUCT_CO @@ -260,6 +272,7 @@ def test_place_order_co_limit(kiteconnect): except Exception as e: warnings.warn(UserWarning("Error while cleaning up orders: {}".format(e))) + # Regular order modify and cancel ################################ @@ -268,11 +281,14 @@ def setup_order_modify_cancel(kiteconnect, variety): symbol = params["exchange"] + ":" + params["tradingsymbol"] ltp = kiteconnect.ltp(symbol) - updated_params = utils.merge_dicts(params, { - "product": kiteconnect.PRODUCT_MIS, - "variety": variety, - "order_type": kiteconnect.ORDER_TYPE_LIMIT - }) + updated_params = utils.merge_dicts( + params, + { + "product": kiteconnect.PRODUCT_MIS, + "variety": variety, + "order_type": kiteconnect.ORDER_TYPE_LIMIT, + }, + ) diff = ltp[symbol]["last_price"] * 0.01 updated_params["price"] = ltp[symbol]["last_price"] - (diff - (diff % 1)) @@ -325,7 +341,9 @@ def test_order_modify_limit_regular(kiteconnect): to_quantity = 2 to_price = updated_params["price"] - 1 - kiteconnect.modify_order(updated_params["variety"], order_id, quantity=to_quantity, price=to_price) + kiteconnect.modify_order( + updated_params["variety"], order_id, quantity=to_quantity, price=to_price + ) time.sleep(0.5) order = kiteconnect.order_history(order_id) @@ -371,7 +389,9 @@ def test_order_modify_limit_amo(kiteconnect): to_quantity = 2 to_price = updated_params["price"] - 1 - kiteconnect.modify_order(updated_params["variety"], order_id, quantity=to_quantity, price=to_price) + kiteconnect.modify_order( + updated_params["variety"], order_id, quantity=to_quantity, price=to_price + ) time.sleep(0.5) order = kiteconnect.order_history(order_id) @@ -383,6 +403,7 @@ def test_order_modify_limit_amo(kiteconnect): except Exception as e: warnings.warn(UserWarning("Error while cleaning up orders: {}".format(e))) + # CO order modify/cancel and exit ################################# @@ -393,7 +414,7 @@ def test_exit_order_co_market_leg(kiteconnect): product=kiteconnect.PRODUCT_MIS, variety=kiteconnect.VARIETY_CO, order_type=kiteconnect.ORDER_TYPE_MARKET, - trigger_price=True + trigger_price=True, ) assert order[-1]["product"] == kiteconnect.PRODUCT_CO @@ -412,7 +433,11 @@ def test_exit_order_co_market_leg(kiteconnect): leg_order = o exit - kiteconnect.exit_order(variety=kiteconnect.VARIETY_CO, order_id=leg_order["order_id"], parent_order_id=order_id) + kiteconnect.exit_order( + variety=kiteconnect.VARIETY_CO, + order_id=leg_order["order_id"], + parent_order_id=order_id, + ) time.sleep(0.5) leg_order_info = kiteconnect.order_history(order_id=leg_order["order_id"]) assert not is_pending_order(leg_order_info[-1]["status"]) @@ -425,7 +450,7 @@ def test_cancel_order_co_limit(kiteconnect): variety=kiteconnect.VARIETY_CO, order_type=kiteconnect.ORDER_TYPE_LIMIT, trigger_price=True, - price=True + price=True, ) status = order[-1]["status"] diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 62fcca4d..78822a49 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -1,19 +1,20 @@ # coding: utf-8 """Pytest config.""" + import os import sys import pytest from kiteconnect import KiteConnect, KiteTicker -sys.path.append(os.path.join(os.path.dirname(__file__), '../helpers')) +sys.path.append(os.path.join(os.path.dirname(__file__), "../helpers")) @pytest.fixture() def kiteconnect(): """Init Kite connect object.""" - kiteconnect = KiteConnect(api_key='', access_token='') - kiteconnect.root = 'http://kite_trade_test' + kiteconnect = KiteConnect(api_key="", access_token="") + kiteconnect.root = "http://kite_trade_test" return kiteconnect @@ -27,8 +28,8 @@ def kiteconnect_with_pooling(): "pool_connections": 20, "pool_maxsize": 10, "max_retries": 2, - "pool_block": False - } + "pool_block": False, + }, ) return kiteconnect @@ -36,7 +37,9 @@ def kiteconnect_with_pooling(): @pytest.fixture() def kiteticker(): """Init Kite ticker object.""" - kws = KiteTicker("", "", "", debug=True, reconnect=False) + kws = KiteTicker( + "", "", "", debug=True, reconnect=False + ) kws.socket_url = "ws://127.0.0.1:9000?api_key=?&user_id=&public_token=" return kws @@ -44,8 +47,7 @@ def kiteticker(): @pytest.fixture() def protocol(): from autobahn.test import FakeTransport - from kiteconnect.ticker import KiteTickerClientProtocol,\ - KiteTickerClientFactory + from kiteconnect.ticker import KiteTickerClientProtocol, KiteTickerClientFactory t = FakeTransport() f = KiteTickerClientFactory() diff --git a/tests/unit/test_connect.py b/tests/unit/test_connect.py index e6b1ba23..218dc262 100644 --- a/tests/unit/test_connect.py +++ b/tests/unit/test_connect.py @@ -21,7 +21,7 @@ def test_positions(kiteconnect): responses.GET, "{0}{1}".format(kiteconnect.root, kiteconnect._routes["portfolio.positions"]), body=utils.get_response("portfolio.positions"), - content_type="application/json" + content_type="application/json", ) positions = kiteconnect.positions() assert type(positions) == dict @@ -36,7 +36,7 @@ def test_holdings(kiteconnect): responses.GET, "{0}{1}".format(kiteconnect.root, kiteconnect._routes["portfolio.holdings"]), body=utils.get_response("portfolio.holdings"), - content_type="application/json" + content_type="application/json", ) holdings = kiteconnect.holdings() assert type(holdings) == list @@ -47,9 +47,11 @@ def test_auction_instruments(kiteconnect): """Test get_auction_instruments.""" responses.add( responses.GET, - "{0}{1}".format(kiteconnect.root, kiteconnect._routes["portfolio.holdings.auction"]), + "{0}{1}".format( + kiteconnect.root, kiteconnect._routes["portfolio.holdings.auction"] + ), body=utils.get_response("portfolio.holdings.auction"), - content_type="application/json" + content_type="application/json", ) auction_inst = kiteconnect.get_auction_instruments() assert type(auction_inst) == list @@ -62,7 +64,7 @@ def test_margins(kiteconnect): responses.GET, "{0}{1}".format(kiteconnect.root, kiteconnect._routes["user.margins"]), body=utils.get_response("user.margins"), - content_type="application/json" + content_type="application/json", ) margins = kiteconnect.margins() assert type(margins) == dict @@ -77,7 +79,7 @@ def test_profile(kiteconnect): responses.GET, "{0}{1}".format(kiteconnect.root, kiteconnect._routes["user.profile"]), body=utils.get_response("user.profile"), - content_type="application/json" + content_type="application/json", ) profile = kiteconnect.profile() assert type(profile) == dict @@ -92,10 +94,10 @@ def test_margins_segmentwise(kiteconnect): kiteconnect.root, kiteconnect._routes["user.margins.segment"].format( segment=kiteconnect.MARGIN_COMMODITY - ) + ), ), body=utils.get_response("user.margins.segment"), - content_type="application/json" + content_type="application/json", ) commodity = kiteconnect.margins(segment=kiteconnect.MARGIN_COMMODITY) assert type(commodity) == dict @@ -108,7 +110,7 @@ def test_orders(kiteconnect): responses.GET, "{0}{1}".format(kiteconnect.root, kiteconnect._routes["orders"]), body=utils.get_response("orders"), - content_type="application/json" + content_type="application/json", ) orders = kiteconnect.orders() assert type(orders) == list @@ -122,7 +124,7 @@ def test_order_history(kiteconnect): responses.GET, "{0}{1}".format(kiteconnect.root, url), body=utils.get_response("order.info"), - content_type="application/json" + content_type="application/json", ) trades = kiteconnect.order_history(order_id="abc123") assert type(trades) == list @@ -135,7 +137,7 @@ def test_trades(kiteconnect): responses.GET, "{0}{1}".format(kiteconnect.root, kiteconnect._routes["trades"]), body=utils.get_response("trades"), - content_type="application/json" + content_type="application/json", ) trades = kiteconnect.trades() assert type(trades) == list @@ -149,7 +151,7 @@ def test_order_trades(kiteconnect): responses.GET, "{0}{1}".format(kiteconnect.root, url), body=utils.get_response("trades"), - content_type="application/json" + content_type="application/json", ) trades = kiteconnect.order_trades(order_id="abc123") assert type(trades) == list @@ -160,9 +162,11 @@ def test_instruments(kiteconnect): """Test mf instruments fetch.""" responses.add( responses.GET, - "{0}{1}".format(kiteconnect.root, kiteconnect._routes["market.instruments.all"]), + "{0}{1}".format( + kiteconnect.root, kiteconnect._routes["market.instruments.all"] + ), body=utils.get_response("market.instruments.all"), - content_type="text/csv" + content_type="text/csv", ) trades = kiteconnect.instruments() assert type(trades) == list @@ -173,10 +177,14 @@ def test_instruments_exchangewise(kiteconnect): """Test mf instruments fetch.""" responses.add( responses.GET, - "{0}{1}".format(kiteconnect.root, - kiteconnect._routes["market.instruments"].format(exchange=kiteconnect.EXCHANGE_NSE)), + "{0}{1}".format( + kiteconnect.root, + kiteconnect._routes["market.instruments"].format( + exchange=kiteconnect.EXCHANGE_NSE + ), + ), body=utils.get_response("market.instruments"), - content_type="text/csv" + content_type="text/csv", ) trades = kiteconnect.instruments(exchange=kiteconnect.EXCHANGE_NSE) assert type(trades) == list @@ -189,7 +197,7 @@ def test_mf_orders(kiteconnect): responses.GET, "{0}{1}".format(kiteconnect.root, kiteconnect._routes["mf.orders"]), body=utils.get_response("mf.orders"), - content_type="application/json" + content_type="application/json", ) trades = kiteconnect.mf_orders() assert type(trades) == list @@ -203,7 +211,7 @@ def test_mf_individual_order(kiteconnect): responses.GET, "{0}{1}".format(kiteconnect.root, url), body=utils.get_response("mf.order.info"), - content_type="application/json" + content_type="application/json", ) trades = kiteconnect.mf_orders(order_id="abc123") assert type(trades) == dict @@ -216,7 +224,7 @@ def test_mf_sips(kiteconnect): responses.GET, "{0}{1}".format(kiteconnect.root, kiteconnect._routes["mf.sips"]), body=utils.get_response("mf.sips"), - content_type="application/json" + content_type="application/json", ) trades = kiteconnect.mf_sips() assert type(trades) == list @@ -230,7 +238,7 @@ def test_mf_individual_sip(kiteconnect): responses.GET, "{0}{1}".format(kiteconnect.root, url), body=utils.get_response("mf.sip.info"), - content_type="application/json" + content_type="application/json", ) trades = kiteconnect.mf_sips(sip_id="abc123") assert type(trades) == dict @@ -243,7 +251,7 @@ def test_mf_holdings(kiteconnect): responses.GET, "{0}{1}".format(kiteconnect.root, kiteconnect._routes["mf.holdings"]), body=utils.get_response("mf.holdings"), - content_type="application/json" + content_type="application/json", ) trades = kiteconnect.mf_holdings() assert type(trades) == list @@ -256,7 +264,7 @@ def test_mf_instruments(kiteconnect): responses.GET, "{0}{1}".format(kiteconnect.root, kiteconnect._routes["mf.instruments"]), body=utils.get_response("mf.instruments"), - content_type="text/csv" + content_type="text/csv", ) trades = kiteconnect.mf_instruments() assert type(trades) == list @@ -269,7 +277,7 @@ def test_get_gtts(kiteconnect): responses.GET, "{0}{1}".format(kiteconnect.root, kiteconnect._routes["gtt"]), body=utils.get_response("gtt"), - content_type="application/json" + content_type="application/json", ) gtts = kiteconnect.get_gtts() assert type(gtts) == list @@ -280,9 +288,11 @@ def test_get_gtt(kiteconnect): """Test single gtt fetch.""" responses.add( responses.GET, - "{0}{1}".format(kiteconnect.root, kiteconnect._routes["gtt.info"].format(trigger_id=123)), + "{0}{1}".format( + kiteconnect.root, kiteconnect._routes["gtt.info"].format(trigger_id=123) + ), body=utils.get_response("gtt.info"), - content_type="application/json" + content_type="application/json", ) gtts = kiteconnect.get_gtt(123) print(gtts) @@ -296,7 +306,7 @@ def test_place_gtt(kiteconnect): responses.POST, "{0}{1}".format(kiteconnect.root, kiteconnect._routes["gtt.place"]), body=utils.get_response("gtt.place"), - content_type="application/json" + content_type="application/json", ) gtts = kiteconnect.place_gtt( trigger_type=kiteconnect.GTT_TYPE_SINGLE, @@ -304,13 +314,15 @@ def test_place_gtt(kiteconnect): exchange="NSE", trigger_values=[1], last_price=800, - orders=[{ - "transaction_type": kiteconnect.TRANSACTION_TYPE_BUY, - "quantity": 1, - "order_type": kiteconnect.ORDER_TYPE_LIMIT, - "product": kiteconnect.PRODUCT_CNC, - "price": 1, - }] + orders=[ + { + "transaction_type": kiteconnect.TRANSACTION_TYPE_BUY, + "quantity": 1, + "order_type": kiteconnect.ORDER_TYPE_LIMIT, + "product": kiteconnect.PRODUCT_CNC, + "price": 1, + } + ], ) assert gtts["trigger_id"] == 123 @@ -320,9 +332,11 @@ def test_modify_gtt(kiteconnect): """Test modify gtt order.""" responses.add( responses.PUT, - "{0}{1}".format(kiteconnect.root, kiteconnect._routes["gtt.modify"].format(trigger_id=123)), + "{0}{1}".format( + kiteconnect.root, kiteconnect._routes["gtt.modify"].format(trigger_id=123) + ), body=utils.get_response("gtt.modify"), - content_type="application/json" + content_type="application/json", ) gtts = kiteconnect.modify_gtt( trigger_id=123, @@ -331,13 +345,15 @@ def test_modify_gtt(kiteconnect): exchange="NSE", trigger_values=[1], last_price=800, - orders=[{ - "transaction_type": kiteconnect.TRANSACTION_TYPE_BUY, - "quantity": 1, - "order_type": kiteconnect.ORDER_TYPE_LIMIT, - "product": kiteconnect.PRODUCT_CNC, - "price": 1, - }] + orders=[ + { + "transaction_type": kiteconnect.TRANSACTION_TYPE_BUY, + "quantity": 1, + "order_type": kiteconnect.ORDER_TYPE_LIMIT, + "product": kiteconnect.PRODUCT_CNC, + "price": 1, + } + ], ) assert gtts["trigger_id"] == 123 @@ -347,9 +363,11 @@ def test_delete_gtt(kiteconnect): """Test delete gtt order.""" responses.add( responses.DELETE, - "{0}{1}".format(kiteconnect.root, kiteconnect._routes["gtt.delete"].format(trigger_id=123)), + "{0}{1}".format( + kiteconnect.root, kiteconnect._routes["gtt.delete"].format(trigger_id=123) + ), body=utils.get_response("gtt.delete"), - content_type="application/json" + content_type="application/json", ) gtts = kiteconnect.delete_gtt(123) assert gtts["trigger_id"] == 123 @@ -357,115 +375,122 @@ def test_delete_gtt(kiteconnect): @responses.activate def test_order_margins(kiteconnect): - """ Test order margins and charges """ + """Test order margins and charges""" responses.add( responses.POST, "{0}{1}".format(kiteconnect.root, kiteconnect._routes["order.margins"]), body=utils.get_response("order.margins"), - content_type="application/json" + content_type="application/json", ) - order_param_single = [{ - "exchange": "NSE", - "tradingsymbol": "INFY", - "transaction_type": "BUY", - "variety": "regular", - "product": "MIS", - "order_type": "MARKET", - "quantity": 2 - }] + order_param_single = [ + { + "exchange": "NSE", + "tradingsymbol": "INFY", + "transaction_type": "BUY", + "variety": "regular", + "product": "MIS", + "order_type": "MARKET", + "quantity": 2, + } + ] margin_detail = kiteconnect.order_margins(order_param_single) # Order margins - assert margin_detail[0]['type'] == "equity" - assert margin_detail[0]['total'] != 0 + assert margin_detail[0]["type"] == "equity" + assert margin_detail[0]["total"] != 0 # Order charges - assert margin_detail[0]['charges']['transaction_tax'] != 0 - assert margin_detail[0]['charges']['gst']['total'] != 0 + assert margin_detail[0]["charges"]["transaction_tax"] != 0 + assert margin_detail[0]["charges"]["gst"]["total"] != 0 @responses.activate def test_basket_order_margins(kiteconnect): - """ Test basket order margins and charges """ + """Test basket order margins and charges""" responses.add( responses.POST, "{0}{1}".format(kiteconnect.root, kiteconnect._routes["order.margins.basket"]), body=utils.get_response("order.margins.basket"), - content_type="application/json" + content_type="application/json", ) - order_param_multi = [{ - "exchange": "NFO", - "tradingsymbol": "NIFTY23JANFUT", - "transaction_type": "BUY", - "variety": "regular", - "product": "MIS", - "order_type": "MARKET", - "quantity": 75 - }, + order_param_multi = [ + { + "exchange": "NFO", + "tradingsymbol": "NIFTY23JANFUT", + "transaction_type": "BUY", + "variety": "regular", + "product": "MIS", + "order_type": "MARKET", + "quantity": 75, + }, { - "exchange": "NFO", - "tradingsymbol": "NIFTY23JANFUT", - "transaction_type": "BUY", - "variety": "regular", - "product": "MIS", - "order_type": "MARKET", - "quantity": 75 - }] + "exchange": "NFO", + "tradingsymbol": "NIFTY23JANFUT", + "transaction_type": "BUY", + "variety": "regular", + "product": "MIS", + "order_type": "MARKET", + "quantity": 75, + }, + ] margin_detail = kiteconnect.basket_order_margins(order_param_multi) # Order margins - assert margin_detail['orders'][0]['exposure'] != 0 - assert margin_detail['orders'][0]['type'] == "equity" + assert margin_detail["orders"][0]["exposure"] != 0 + assert margin_detail["orders"][0]["type"] == "equity" # Order charges - assert margin_detail['orders'][0]['total'] != 0 + assert margin_detail["orders"][0]["total"] != 0 + @responses.activate def test_virtual_contract_note(kiteconnect): - """ Test virtual contract note charges """ + """Test virtual contract note charges""" responses.add( responses.POST, "{0}{1}".format(kiteconnect.root, kiteconnect._routes["order.contract_note"]), body=utils.get_response("order.contract_note"), - content_type="application/json" + content_type="application/json", ) - order_book_params = [{ - "order_id": "111111111", - "exchange": "NSE", - "tradingsymbol": "SBIN", - "transaction_type": "BUY", - "variety": "regular", - "product": "CNC", - "order_type": "MARKET", - "quantity": 1, - "average_price": 560 - }, - { - "order_id": "2222222222", - "exchange": "MCX", - "tradingsymbol": "GOLDPETAL23JULFUT", - "transaction_type": "SELL", - "variety": "regular", - "product": "NRML", - "order_type": "LIMIT", - "quantity": 1, - "average_price": 5862 - }, - { - "order_id": "3333333333", - "exchange": "NFO", - "tradingsymbol": "NIFTY2371317900PE", - "transaction_type": "BUY", - "variety": "regular", - "product": "NRML", - "order_type": "LIMIT", - "quantity": 100, - "average_price": 1.5 - }] + order_book_params = [ + { + "order_id": "111111111", + "exchange": "NSE", + "tradingsymbol": "SBIN", + "transaction_type": "BUY", + "variety": "regular", + "product": "CNC", + "order_type": "MARKET", + "quantity": 1, + "average_price": 560, + }, + { + "order_id": "2222222222", + "exchange": "MCX", + "tradingsymbol": "GOLDPETAL23JULFUT", + "transaction_type": "SELL", + "variety": "regular", + "product": "NRML", + "order_type": "LIMIT", + "quantity": 1, + "average_price": 5862, + }, + { + "order_id": "3333333333", + "exchange": "NFO", + "tradingsymbol": "NIFTY2371317900PE", + "transaction_type": "BUY", + "variety": "regular", + "product": "NRML", + "order_type": "LIMIT", + "quantity": 100, + "average_price": 1.5, + }, + ] order_book_charges = kiteconnect.get_virtual_contract_note(order_book_params) # Order charges - assert order_book_charges[0]['charges']['transaction_tax_type'] == "stt" - assert order_book_charges[0]['charges']['total'] != 0 + assert order_book_charges[0]["charges"]["transaction_tax_type"] == "stt" + assert order_book_charges[0]["charges"]["total"] != 0 # CTT tax type - assert order_book_charges[1]['charges']['transaction_tax_type'] == "ctt" - assert order_book_charges[1]['charges']['total'] != 0 + assert order_book_charges[1]["charges"]["transaction_tax_type"] == "ctt" + assert order_book_charges[1]["charges"]["total"] != 0 diff --git a/tests/unit/test_exceptions.py b/tests/unit/test_exceptions.py index 4b4d3420..150af03a 100644 --- a/tests/unit/test_exceptions.py +++ b/tests/unit/test_exceptions.py @@ -11,12 +11,14 @@ def test_wrong_json_response(kiteconnect): responses.GET, "%s%s" % (kiteconnect.root, kiteconnect._routes["portfolio.positions"]), body="{a:b}", - content_type="application/json" + content_type="application/json", ) with pytest.raises(ex.DataException) as exc: kiteconnect.positions() - assert exc.message == "Couldn't parse the JSON response "\ + assert ( + exc.message == "Couldn't parse the JSON response " "received from the server: {a:b}" + ) @responses.activate @@ -26,30 +28,35 @@ def test_wrong_content_type(kiteconnect): responses.GET, "%s%s" % (kiteconnect.root, kiteconnect._routes["portfolio.positions"]), body=rdf_data, - content_type="application/rdf+xml" + content_type="application/rdf+xml", ) with pytest.raises(ex.DataException) as exc: kiteconnect.positions() - assert exc.message == "Unknown Content-Type ({content_type}) with response: ({content})".format( - content_type='application/rdf+xml', - content=rdf_data + assert ( + exc.message + == "Unknown Content-Type ({content_type}) with response: ({content})".format( + content_type="application/rdf+xml", content=rdf_data + ) ) -@pytest.mark.parametrize("error_type,message", [ - ('PermissionException', 'oops! permission issue'), - ('OrderException', 'oops! cannot place order'), - ('InputException', 'missing or invalid params'), - ('NetworkException', 'oopsy doopsy network issues damn!'), - ('CustomException', 'this is an exception i just created') -]) +@pytest.mark.parametrize( + "error_type,message", + [ + ("PermissionException", "oops! permission issue"), + ("OrderException", "oops! cannot place order"), + ("InputException", "missing or invalid params"), + ("NetworkException", "oopsy doopsy network issues damn!"), + ("CustomException", "this is an exception i just created"), + ], +) @responses.activate def test_native_exceptions(error_type, message, kiteconnect): responses.add( responses.GET, "%s%s" % (kiteconnect.root, kiteconnect._routes["portfolio.positions"]), body='{"error_type": "%s", "message": "%s"}' % (error_type, message), - content_type="application/json" + content_type="application/json", ) with pytest.raises(getattr(ex, error_type, ex.GeneralException)) as exc: kiteconnect.positions() diff --git a/tests/unit/test_kite_object.py b/tests/unit/test_kite_object.py index 04c0214d..4fdd9203 100644 --- a/tests/unit/test_kite_object.py +++ b/tests/unit/test_kite_object.py @@ -9,10 +9,7 @@ def get_fake_token(self, route, params=None): - return { - "access_token": "TOKEN", - "login_time": None - } + return {"access_token": "TOKEN", "login_time": None} def get_fake_delete(self, route, params=None): @@ -20,9 +17,11 @@ def get_fake_delete(self, route, params=None): class TestKiteConnectObject: - def test_login_url(self, kiteconnect): - assert kiteconnect.login_url() == "https://kite.zerodha.com/connect/login?api_key=&v=3" + assert ( + kiteconnect.login_url() + == "https://kite.zerodha.com/connect/login?api_key=&v=3" + ) def test_request_without_pooling(self, kiteconnect): assert isinstance(kiteconnect.reqsession, requests.Session) is True @@ -31,7 +30,7 @@ def test_request_without_pooling(self, kiteconnect): def test_request_pooling(self, kiteconnect_with_pooling): assert isinstance(kiteconnect_with_pooling.reqsession, requests.Session) is True assert kiteconnect_with_pooling.reqsession.request is not None - http_adapter = kiteconnect_with_pooling.reqsession.adapters['https://'] + http_adapter = kiteconnect_with_pooling.reqsession.adapters["https://"] assert http_adapter._pool_maxsize == 10 assert http_adapter._pool_connections == 20 assert http_adapter._pool_block is False @@ -39,7 +38,6 @@ def test_request_pooling(self, kiteconnect_with_pooling): @responses.activate def test_set_session_expiry_hook_meth(self, kiteconnect): - def mock_hook(): raise ex.TokenException("token expired it seems! please login again") @@ -48,10 +46,12 @@ def mock_hook(): # Now lets try raising TokenException responses.add( responses.GET, - "{0}{1}".format(kiteconnect.root, kiteconnect._routes["portfolio.positions"]), + "{0}{1}".format( + kiteconnect.root, kiteconnect._routes["portfolio.positions"] + ), body='{"error_type": "TokenException", "message": "Please login again"}', content_type="application/json", - status=403 + status=403, ) with pytest.raises(ex.TokenException) as exc: kiteconnect.positions() @@ -68,8 +68,7 @@ def test_set_access_token_meth(self, kiteconnect): @patch.object(KiteConnect, "_post", get_fake_token) def test_generate_session(self, kiteconnect): resp = kiteconnect.generate_session( - request_token="", - api_secret="" + request_token="", api_secret="" ) assert resp["access_token"] == "TOKEN" assert kiteconnect.access_token == "TOKEN" diff --git a/tests/unit/test_ticker.py b/tests/unit/test_ticker.py index 58f928ec..ddabbf2b 100644 --- a/tests/unit/test_ticker.py +++ b/tests/unit/test_ticker.py @@ -1,5 +1,6 @@ # coding: utf-8 """Ticker tests""" + import six import json from mock import Mock @@ -10,13 +11,12 @@ class TestTicker: - def test_autoping(self, protocol): protocol.autoPingInterval = 1 protocol.websocket_protocols = [Mock()] protocol.websocket_extensions = [] protocol._onOpen = lambda: None - protocol._wskey = '0' * 24 + protocol._wskey = "0" * 24 protocol.peer = Mock() # usually provided by the Twisted or asyncio specific @@ -32,7 +32,9 @@ def test_autoping(self, protocol): b"HTTP/1.1 101 Switching Protocols\x0d\x0a" b"Upgrade: websocket\x0d\x0a" b"Connection: upgrade\x0d\x0a" - b"Sec-Websocket-Accept: " + b64encode(sha1(key).digest()) + b"\x0d\x0a\x0d\x0a" + b"Sec-Websocket-Accept: " + + b64encode(sha1(key).digest()) + + b"\x0d\x0a\x0d\x0a" ) protocol.processHandshake() diff --git a/uv.lock b/uv.lock new file mode 100644 index 00000000..87af50ba --- /dev/null +++ b/uv.lock @@ -0,0 +1,723 @@ +version = 1 +revision = 1 +requires-python = ">=3.10" + +[[package]] +name = "attrs" +version = "25.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815 }, +] + +[[package]] +name = "certifi" +version = "2025.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216 }, +] + +[[package]] +name = "cffi" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191 }, + { url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592 }, + { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024 }, + { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188 }, + { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571 }, + { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687 }, + { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211 }, + { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325 }, + { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784 }, + { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564 }, + { url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804 }, + { url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299 }, + { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264 }, + { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651 }, + { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259 }, + { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200 }, + { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235 }, + { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721 }, + { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242 }, + { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999 }, + { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242 }, + { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604 }, + { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727 }, + { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400 }, + { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178 }, + { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840 }, + { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 }, + { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 }, + { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 }, + { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256 }, + { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 }, + { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 }, + { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 }, + { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448 }, + { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976 }, + { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 }, + { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 }, + { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 }, + { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 }, + { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 }, + { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 }, + { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 }, + { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 }, + { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 }, + { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 }, + { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/98/f3b8013223728a99b908c9344da3aa04ee6e3fa235f19409033eda92fb78/charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72", size = 207695 }, + { url = "https://files.pythonhosted.org/packages/21/40/5188be1e3118c82dcb7c2a5ba101b783822cfb413a0268ed3be0468532de/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe", size = 147153 }, + { url = "https://files.pythonhosted.org/packages/37/60/5d0d74bc1e1380f0b72c327948d9c2aca14b46a9efd87604e724260f384c/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601", size = 160428 }, + { url = "https://files.pythonhosted.org/packages/85/9a/d891f63722d9158688de58d050c59dc3da560ea7f04f4c53e769de5140f5/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c", size = 157627 }, + { url = "https://files.pythonhosted.org/packages/65/1a/7425c952944a6521a9cfa7e675343f83fd82085b8af2b1373a2409c683dc/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2", size = 152388 }, + { url = "https://files.pythonhosted.org/packages/f0/c9/a2c9c2a355a8594ce2446085e2ec97fd44d323c684ff32042e2a6b718e1d/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0", size = 150077 }, + { url = "https://files.pythonhosted.org/packages/3b/38/20a1f44e4851aa1c9105d6e7110c9d020e093dfa5836d712a5f074a12bf7/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0", size = 161631 }, + { url = "https://files.pythonhosted.org/packages/a4/fa/384d2c0f57edad03d7bec3ebefb462090d8905b4ff5a2d2525f3bb711fac/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0", size = 159210 }, + { url = "https://files.pythonhosted.org/packages/33/9e/eca49d35867ca2db336b6ca27617deed4653b97ebf45dfc21311ce473c37/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a", size = 153739 }, + { url = "https://files.pythonhosted.org/packages/2a/91/26c3036e62dfe8de8061182d33be5025e2424002125c9500faff74a6735e/charset_normalizer-3.4.3-cp310-cp310-win32.whl", hash = "sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f", size = 99825 }, + { url = "https://files.pythonhosted.org/packages/e2/c6/f05db471f81af1fa01839d44ae2a8bfeec8d2a8b4590f16c4e7393afd323/charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669", size = 107452 }, + { url = "https://files.pythonhosted.org/packages/7f/b5/991245018615474a60965a7c9cd2b4efbaabd16d582a5547c47ee1c7730b/charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b", size = 204483 }, + { url = "https://files.pythonhosted.org/packages/c7/2a/ae245c41c06299ec18262825c1569c5d3298fc920e4ddf56ab011b417efd/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64", size = 145520 }, + { url = "https://files.pythonhosted.org/packages/3a/a4/b3b6c76e7a635748c4421d2b92c7b8f90a432f98bda5082049af37ffc8e3/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91", size = 158876 }, + { url = "https://files.pythonhosted.org/packages/e2/e6/63bb0e10f90a8243c5def74b5b105b3bbbfb3e7bb753915fe333fb0c11ea/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f", size = 156083 }, + { url = "https://files.pythonhosted.org/packages/87/df/b7737ff046c974b183ea9aa111b74185ac8c3a326c6262d413bd5a1b8c69/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07", size = 150295 }, + { url = "https://files.pythonhosted.org/packages/61/f1/190d9977e0084d3f1dc169acd060d479bbbc71b90bf3e7bf7b9927dec3eb/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30", size = 148379 }, + { url = "https://files.pythonhosted.org/packages/4c/92/27dbe365d34c68cfe0ca76f1edd70e8705d82b378cb54ebbaeabc2e3029d/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14", size = 160018 }, + { url = "https://files.pythonhosted.org/packages/99/04/baae2a1ea1893a01635d475b9261c889a18fd48393634b6270827869fa34/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c", size = 157430 }, + { url = "https://files.pythonhosted.org/packages/2f/36/77da9c6a328c54d17b960c89eccacfab8271fdaaa228305330915b88afa9/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae", size = 151600 }, + { url = "https://files.pythonhosted.org/packages/64/d4/9eb4ff2c167edbbf08cdd28e19078bf195762e9bd63371689cab5ecd3d0d/charset_normalizer-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849", size = 99616 }, + { url = "https://files.pythonhosted.org/packages/f4/9c/996a4a028222e7761a96634d1820de8a744ff4327a00ada9c8942033089b/charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c", size = 107108 }, + { url = "https://files.pythonhosted.org/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", size = 205655 }, + { url = "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", size = 146223 }, + { url = "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", size = 159366 }, + { url = "https://files.pythonhosted.org/packages/82/10/0fd19f20c624b278dddaf83b8464dcddc2456cb4b02bb902a6da126b87a1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", size = 157104 }, + { url = "https://files.pythonhosted.org/packages/16/ab/0233c3231af734f5dfcf0844aa9582d5a1466c985bbed6cedab85af9bfe3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", size = 151830 }, + { url = "https://files.pythonhosted.org/packages/ae/02/e29e22b4e02839a0e4a06557b1999d0a47db3567e82989b5bb21f3fbbd9f/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", size = 148854 }, + { url = "https://files.pythonhosted.org/packages/05/6b/e2539a0a4be302b481e8cafb5af8792da8093b486885a1ae4d15d452bcec/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", size = 160670 }, + { url = "https://files.pythonhosted.org/packages/31/e7/883ee5676a2ef217a40ce0bffcc3d0dfbf9e64cbcfbdf822c52981c3304b/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", size = 158501 }, + { url = "https://files.pythonhosted.org/packages/c1/35/6525b21aa0db614cf8b5792d232021dca3df7f90a1944db934efa5d20bb1/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", size = 153173 }, + { url = "https://files.pythonhosted.org/packages/50/ee/f4704bad8201de513fdc8aac1cabc87e38c5818c93857140e06e772b5892/charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37", size = 99822 }, + { url = "https://files.pythonhosted.org/packages/39/f5/3b3836ca6064d0992c58c7561c6b6eee1b3892e9665d650c803bd5614522/charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc", size = 107543 }, + { url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326 }, + { url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008 }, + { url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196 }, + { url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819 }, + { url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350 }, + { url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644 }, + { url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468 }, + { url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187 }, + { url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699 }, + { url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580 }, + { url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366 }, + { url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342 }, + { url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995 }, + { url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640 }, + { url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636 }, + { url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939 }, + { url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580 }, + { url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870 }, + { url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797 }, + { url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224 }, + { url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086 }, + { url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400 }, + { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "coverage" +version = "7.10.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/61/83/153f54356c7c200013a752ce1ed5448573dca546ce125801afca9e1ac1a4/coverage-7.10.5.tar.gz", hash = "sha256:f2e57716a78bc3ae80b2207be0709a3b2b63b9f2dcf9740ee6ac03588a2015b6", size = 821662 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/70/e77b0061a6c7157bfce645c6b9a715a08d4c86b3360a7b3252818080b817/coverage-7.10.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c6a5c3414bfc7451b879141ce772c546985163cf553f08e0f135f0699a911801", size = 216774 }, + { url = "https://files.pythonhosted.org/packages/91/08/2a79de5ecf37ee40f2d898012306f11c161548753391cec763f92647837b/coverage-7.10.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bc8e4d99ce82f1710cc3c125adc30fd1487d3cf6c2cd4994d78d68a47b16989a", size = 217175 }, + { url = "https://files.pythonhosted.org/packages/64/57/0171d69a699690149a6ba6a4eb702814448c8d617cf62dbafa7ce6bfdf63/coverage-7.10.5-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:02252dc1216e512a9311f596b3169fad54abcb13827a8d76d5630c798a50a754", size = 243931 }, + { url = "https://files.pythonhosted.org/packages/15/06/3a67662c55656702bd398a727a7f35df598eb11104fcb34f1ecbb070291a/coverage-7.10.5-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:73269df37883e02d460bee0cc16be90509faea1e3bd105d77360b512d5bb9c33", size = 245740 }, + { url = "https://files.pythonhosted.org/packages/00/f4/f8763aabf4dc30ef0d0012522d312f0b7f9fede6246a1f27dbcc4a1e523c/coverage-7.10.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f8a81b0614642f91c9effd53eec284f965577591f51f547a1cbeb32035b4c2f", size = 247600 }, + { url = "https://files.pythonhosted.org/packages/9c/31/6632219a9065e1b83f77eda116fed4c76fb64908a6a9feae41816dab8237/coverage-7.10.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6a29f8e0adb7f8c2b95fa2d4566a1d6e6722e0a637634c6563cb1ab844427dd9", size = 245640 }, + { url = "https://files.pythonhosted.org/packages/6e/e2/3dba9b86037b81649b11d192bb1df11dde9a81013e434af3520222707bc8/coverage-7.10.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fcf6ab569436b4a647d4e91accba12509ad9f2554bc93d3aee23cc596e7f99c3", size = 243659 }, + { url = "https://files.pythonhosted.org/packages/02/b9/57170bd9f3e333837fc24ecc88bc70fbc2eb7ccfd0876854b0c0407078c3/coverage-7.10.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:90dc3d6fb222b194a5de60af8d190bedeeddcbc7add317e4a3cd333ee6b7c879", size = 244537 }, + { url = "https://files.pythonhosted.org/packages/b3/1c/93ac36ef1e8b06b8d5777393a3a40cb356f9f3dab980be40a6941e443588/coverage-7.10.5-cp310-cp310-win32.whl", hash = "sha256:414a568cd545f9dc75f0686a0049393de8098414b58ea071e03395505b73d7a8", size = 219285 }, + { url = "https://files.pythonhosted.org/packages/30/95/23252277e6e5fe649d6cd3ed3f35d2307e5166de4e75e66aa7f432abc46d/coverage-7.10.5-cp310-cp310-win_amd64.whl", hash = "sha256:e551f9d03347196271935fd3c0c165f0e8c049220280c1120de0084d65e9c7ff", size = 220185 }, + { url = "https://files.pythonhosted.org/packages/cb/f2/336d34d2fc1291ca7c18eeb46f64985e6cef5a1a7ef6d9c23720c6527289/coverage-7.10.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c177e6ffe2ebc7c410785307758ee21258aa8e8092b44d09a2da767834f075f2", size = 216890 }, + { url = "https://files.pythonhosted.org/packages/39/ea/92448b07cc1cf2b429d0ce635f59cf0c626a5d8de21358f11e92174ff2a6/coverage-7.10.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:14d6071c51ad0f703d6440827eaa46386169b5fdced42631d5a5ac419616046f", size = 217287 }, + { url = "https://files.pythonhosted.org/packages/96/ba/ad5b36537c5179c808d0ecdf6e4aa7630b311b3c12747ad624dcd43a9b6b/coverage-7.10.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:61f78c7c3bc272a410c5ae3fde7792b4ffb4acc03d35a7df73ca8978826bb7ab", size = 247683 }, + { url = "https://files.pythonhosted.org/packages/28/e5/fe3bbc8d097029d284b5fb305b38bb3404895da48495f05bff025df62770/coverage-7.10.5-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f39071caa126f69d63f99b324fb08c7b1da2ec28cbb1fe7b5b1799926492f65c", size = 249614 }, + { url = "https://files.pythonhosted.org/packages/69/9c/a1c89a8c8712799efccb32cd0a1ee88e452f0c13a006b65bb2271f1ac767/coverage-7.10.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:343a023193f04d46edc46b2616cdbee68c94dd10208ecd3adc56fcc54ef2baa1", size = 251719 }, + { url = "https://files.pythonhosted.org/packages/e9/be/5576b5625865aa95b5633315f8f4142b003a70c3d96e76f04487c3b5cc95/coverage-7.10.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:585ffe93ae5894d1ebdee69fc0b0d4b7c75d8007983692fb300ac98eed146f78", size = 249411 }, + { url = "https://files.pythonhosted.org/packages/94/0a/e39a113d4209da0dbbc9385608cdb1b0726a4d25f78672dc51c97cfea80f/coverage-7.10.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b0ef4e66f006ed181df29b59921bd8fc7ed7cd6a9289295cd8b2824b49b570df", size = 247466 }, + { url = "https://files.pythonhosted.org/packages/40/cb/aebb2d8c9e3533ee340bea19b71c5b76605a0268aa49808e26fe96ec0a07/coverage-7.10.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:eb7b0bbf7cc1d0453b843eca7b5fa017874735bef9bfdfa4121373d2cc885ed6", size = 248104 }, + { url = "https://files.pythonhosted.org/packages/08/e6/26570d6ccce8ff5de912cbfd268e7f475f00597cb58da9991fa919c5e539/coverage-7.10.5-cp311-cp311-win32.whl", hash = "sha256:1d043a8a06987cc0c98516e57c4d3fc2c1591364831e9deb59c9e1b4937e8caf", size = 219327 }, + { url = "https://files.pythonhosted.org/packages/79/79/5f48525e366e518b36e66167e3b6e5db6fd54f63982500c6a5abb9d3dfbd/coverage-7.10.5-cp311-cp311-win_amd64.whl", hash = "sha256:fefafcca09c3ac56372ef64a40f5fe17c5592fab906e0fdffd09543f3012ba50", size = 220213 }, + { url = "https://files.pythonhosted.org/packages/40/3c/9058128b7b0bf333130c320b1eb1ae485623014a21ee196d68f7737f8610/coverage-7.10.5-cp311-cp311-win_arm64.whl", hash = "sha256:7e78b767da8b5fc5b2faa69bb001edafcd6f3995b42a331c53ef9572c55ceb82", size = 218893 }, + { url = "https://files.pythonhosted.org/packages/27/8e/40d75c7128f871ea0fd829d3e7e4a14460cad7c3826e3b472e6471ad05bd/coverage-7.10.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c2d05c7e73c60a4cecc7d9b60dbfd603b4ebc0adafaef371445b47d0f805c8a9", size = 217077 }, + { url = "https://files.pythonhosted.org/packages/18/a8/f333f4cf3fb5477a7f727b4d603a2eb5c3c5611c7fe01329c2e13b23b678/coverage-7.10.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:32ddaa3b2c509778ed5373b177eb2bf5662405493baeff52278a0b4f9415188b", size = 217310 }, + { url = "https://files.pythonhosted.org/packages/ec/2c/fbecd8381e0a07d1547922be819b4543a901402f63930313a519b937c668/coverage-7.10.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:dd382410039fe062097aa0292ab6335a3f1e7af7bba2ef8d27dcda484918f20c", size = 248802 }, + { url = "https://files.pythonhosted.org/packages/3f/bc/1011da599b414fb6c9c0f34086736126f9ff71f841755786a6b87601b088/coverage-7.10.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7fa22800f3908df31cea6fb230f20ac49e343515d968cc3a42b30d5c3ebf9b5a", size = 251550 }, + { url = "https://files.pythonhosted.org/packages/4c/6f/b5c03c0c721c067d21bc697accc3642f3cef9f087dac429c918c37a37437/coverage-7.10.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f366a57ac81f5e12797136552f5b7502fa053c861a009b91b80ed51f2ce651c6", size = 252684 }, + { url = "https://files.pythonhosted.org/packages/f9/50/d474bc300ebcb6a38a1047d5c465a227605d6473e49b4e0d793102312bc5/coverage-7.10.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5f1dc8f1980a272ad4a6c84cba7981792344dad33bf5869361576b7aef42733a", size = 250602 }, + { url = "https://files.pythonhosted.org/packages/4a/2d/548c8e04249cbba3aba6bd799efdd11eee3941b70253733f5d355d689559/coverage-7.10.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2285c04ee8676f7938b02b4936d9b9b672064daab3187c20f73a55f3d70e6b4a", size = 248724 }, + { url = "https://files.pythonhosted.org/packages/e2/96/a7c3c0562266ac39dcad271d0eec8fc20ab576e3e2f64130a845ad2a557b/coverage-7.10.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c2492e4dd9daab63f5f56286f8a04c51323d237631eb98505d87e4c4ff19ec34", size = 250158 }, + { url = "https://files.pythonhosted.org/packages/f3/75/74d4be58c70c42ef0b352d597b022baf12dbe2b43e7cb1525f56a0fb1d4b/coverage-7.10.5-cp312-cp312-win32.whl", hash = "sha256:38a9109c4ee8135d5df5505384fc2f20287a47ccbe0b3f04c53c9a1989c2bbaf", size = 219493 }, + { url = "https://files.pythonhosted.org/packages/4f/08/364e6012d1d4d09d1e27437382967efed971d7613f94bca9add25f0c1f2b/coverage-7.10.5-cp312-cp312-win_amd64.whl", hash = "sha256:6b87f1ad60b30bc3c43c66afa7db6b22a3109902e28c5094957626a0143a001f", size = 220302 }, + { url = "https://files.pythonhosted.org/packages/db/d5/7c8a365e1f7355c58af4fe5faf3f90cc8e587590f5854808d17ccb4e7077/coverage-7.10.5-cp312-cp312-win_arm64.whl", hash = "sha256:672a6c1da5aea6c629819a0e1461e89d244f78d7b60c424ecf4f1f2556c041d8", size = 218936 }, + { url = "https://files.pythonhosted.org/packages/9f/08/4166ecfb60ba011444f38a5a6107814b80c34c717bc7a23be0d22e92ca09/coverage-7.10.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ef3b83594d933020f54cf65ea1f4405d1f4e41a009c46df629dd964fcb6e907c", size = 217106 }, + { url = "https://files.pythonhosted.org/packages/25/d7/b71022408adbf040a680b8c64bf6ead3be37b553e5844f7465643979f7ca/coverage-7.10.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2b96bfdf7c0ea9faebce088a3ecb2382819da4fbc05c7b80040dbc428df6af44", size = 217353 }, + { url = "https://files.pythonhosted.org/packages/74/68/21e0d254dbf8972bb8dd95e3fe7038f4be037ff04ba47d6d1b12b37510ba/coverage-7.10.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:63df1fdaffa42d914d5c4d293e838937638bf75c794cf20bee12978fc8c4e3bc", size = 248350 }, + { url = "https://files.pythonhosted.org/packages/90/65/28752c3a896566ec93e0219fc4f47ff71bd2b745f51554c93e8dcb659796/coverage-7.10.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8002dc6a049aac0e81ecec97abfb08c01ef0c1fbf962d0c98da3950ace89b869", size = 250955 }, + { url = "https://files.pythonhosted.org/packages/a5/eb/ca6b7967f57f6fef31da8749ea20417790bb6723593c8cd98a987be20423/coverage-7.10.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:63d4bb2966d6f5f705a6b0c6784c8969c468dbc4bcf9d9ded8bff1c7e092451f", size = 252230 }, + { url = "https://files.pythonhosted.org/packages/bc/29/17a411b2a2a18f8b8c952aa01c00f9284a1fbc677c68a0003b772ea89104/coverage-7.10.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1f672efc0731a6846b157389b6e6d5d5e9e59d1d1a23a5c66a99fd58339914d5", size = 250387 }, + { url = "https://files.pythonhosted.org/packages/c7/89/97a9e271188c2fbb3db82235c33980bcbc733da7da6065afbaa1d685a169/coverage-7.10.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3f39cef43d08049e8afc1fde4a5da8510fc6be843f8dea350ee46e2a26b2f54c", size = 248280 }, + { url = "https://files.pythonhosted.org/packages/d1/c6/0ad7d0137257553eb4706b4ad6180bec0a1b6a648b092c5bbda48d0e5b2c/coverage-7.10.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2968647e3ed5a6c019a419264386b013979ff1fb67dd11f5c9886c43d6a31fc2", size = 249894 }, + { url = "https://files.pythonhosted.org/packages/84/56/fb3aba936addb4c9e5ea14f5979393f1c2466b4c89d10591fd05f2d6b2aa/coverage-7.10.5-cp313-cp313-win32.whl", hash = "sha256:0d511dda38595b2b6934c2b730a1fd57a3635c6aa2a04cb74714cdfdd53846f4", size = 219536 }, + { url = "https://files.pythonhosted.org/packages/fc/54/baacb8f2f74431e3b175a9a2881feaa8feb6e2f187a0e7e3046f3c7742b2/coverage-7.10.5-cp313-cp313-win_amd64.whl", hash = "sha256:9a86281794a393513cf117177fd39c796b3f8e3759bb2764259a2abba5cce54b", size = 220330 }, + { url = "https://files.pythonhosted.org/packages/64/8a/82a3788f8e31dee51d350835b23d480548ea8621f3effd7c3ba3f7e5c006/coverage-7.10.5-cp313-cp313-win_arm64.whl", hash = "sha256:cebd8e906eb98bb09c10d1feed16096700b1198d482267f8bf0474e63a7b8d84", size = 218961 }, + { url = "https://files.pythonhosted.org/packages/d8/a1/590154e6eae07beee3b111cc1f907c30da6fc8ce0a83ef756c72f3c7c748/coverage-7.10.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0520dff502da5e09d0d20781df74d8189ab334a1e40d5bafe2efaa4158e2d9e7", size = 217819 }, + { url = "https://files.pythonhosted.org/packages/0d/ff/436ffa3cfc7741f0973c5c89405307fe39b78dcf201565b934e6616fc4ad/coverage-7.10.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d9cd64aca68f503ed3f1f18c7c9174cbb797baba02ca8ab5112f9d1c0328cd4b", size = 218040 }, + { url = "https://files.pythonhosted.org/packages/a0/ca/5787fb3d7820e66273913affe8209c534ca11241eb34ee8c4fd2aaa9dd87/coverage-7.10.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0913dd1613a33b13c4f84aa6e3f4198c1a21ee28ccb4f674985c1f22109f0aae", size = 259374 }, + { url = "https://files.pythonhosted.org/packages/b5/89/21af956843896adc2e64fc075eae3c1cadb97ee0a6960733e65e696f32dd/coverage-7.10.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1b7181c0feeb06ed8a02da02792f42f829a7b29990fef52eff257fef0885d760", size = 261551 }, + { url = "https://files.pythonhosted.org/packages/e1/96/390a69244ab837e0ac137989277879a084c786cf036c3c4a3b9637d43a89/coverage-7.10.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36d42b7396b605f774d4372dd9c49bed71cbabce4ae1ccd074d155709dd8f235", size = 263776 }, + { url = "https://files.pythonhosted.org/packages/00/32/cfd6ae1da0a521723349f3129b2455832fc27d3f8882c07e5b6fefdd0da2/coverage-7.10.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b4fdc777e05c4940b297bf47bf7eedd56a39a61dc23ba798e4b830d585486ca5", size = 261326 }, + { url = "https://files.pythonhosted.org/packages/4c/c4/bf8d459fb4ce2201e9243ce6c015936ad283a668774430a3755f467b39d1/coverage-7.10.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:42144e8e346de44a6f1dbd0a56575dd8ab8dfa7e9007da02ea5b1c30ab33a7db", size = 259090 }, + { url = "https://files.pythonhosted.org/packages/f4/5d/a234f7409896468e5539d42234016045e4015e857488b0b5b5f3f3fa5f2b/coverage-7.10.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:66c644cbd7aed8fe266d5917e2c9f65458a51cfe5eeff9c05f15b335f697066e", size = 260217 }, + { url = "https://files.pythonhosted.org/packages/f3/ad/87560f036099f46c2ddd235be6476dd5c1d6be6bb57569a9348d43eeecea/coverage-7.10.5-cp313-cp313t-win32.whl", hash = "sha256:2d1b73023854068c44b0c554578a4e1ef1b050ed07cf8b431549e624a29a66ee", size = 220194 }, + { url = "https://files.pythonhosted.org/packages/36/a8/04a482594fdd83dc677d4a6c7e2d62135fff5a1573059806b8383fad9071/coverage-7.10.5-cp313-cp313t-win_amd64.whl", hash = "sha256:54a1532c8a642d8cc0bd5a9a51f5a9dcc440294fd06e9dda55e743c5ec1a8f14", size = 221258 }, + { url = "https://files.pythonhosted.org/packages/eb/ad/7da28594ab66fe2bc720f1bc9b131e62e9b4c6e39f044d9a48d18429cc21/coverage-7.10.5-cp313-cp313t-win_arm64.whl", hash = "sha256:74d5b63fe3f5f5d372253a4ef92492c11a4305f3550631beaa432fc9df16fcff", size = 219521 }, + { url = "https://files.pythonhosted.org/packages/d3/7f/c8b6e4e664b8a95254c35a6c8dd0bf4db201ec681c169aae2f1256e05c85/coverage-7.10.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:68c5e0bc5f44f68053369fa0d94459c84548a77660a5f2561c5e5f1e3bed7031", size = 217090 }, + { url = "https://files.pythonhosted.org/packages/44/74/3ee14ede30a6e10a94a104d1d0522d5fb909a7c7cac2643d2a79891ff3b9/coverage-7.10.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cf33134ffae93865e32e1e37df043bef15a5e857d8caebc0099d225c579b0fa3", size = 217365 }, + { url = "https://files.pythonhosted.org/packages/41/5f/06ac21bf87dfb7620d1f870dfa3c2cae1186ccbcdc50b8b36e27a0d52f50/coverage-7.10.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ad8fa9d5193bafcf668231294241302b5e683a0518bf1e33a9a0dfb142ec3031", size = 248413 }, + { url = "https://files.pythonhosted.org/packages/21/bc/cc5bed6e985d3a14228539631573f3863be6a2587381e8bc5fdf786377a1/coverage-7.10.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:146fa1531973d38ab4b689bc764592fe6c2f913e7e80a39e7eeafd11f0ef6db2", size = 250943 }, + { url = "https://files.pythonhosted.org/packages/8d/43/6a9fc323c2c75cd80b18d58db4a25dc8487f86dd9070f9592e43e3967363/coverage-7.10.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6013a37b8a4854c478d3219ee8bc2392dea51602dd0803a12d6f6182a0061762", size = 252301 }, + { url = "https://files.pythonhosted.org/packages/69/7c/3e791b8845f4cd515275743e3775adb86273576596dc9f02dca37357b4f2/coverage-7.10.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:eb90fe20db9c3d930fa2ad7a308207ab5b86bf6a76f54ab6a40be4012d88fcae", size = 250302 }, + { url = "https://files.pythonhosted.org/packages/5c/bc/5099c1e1cb0c9ac6491b281babea6ebbf999d949bf4aa8cdf4f2b53505e8/coverage-7.10.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:384b34482272e960c438703cafe63316dfbea124ac62006a455c8410bf2a2262", size = 248237 }, + { url = "https://files.pythonhosted.org/packages/7e/51/d346eb750a0b2f1e77f391498b753ea906fde69cc11e4b38dca28c10c88c/coverage-7.10.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:467dc74bd0a1a7de2bedf8deaf6811f43602cb532bd34d81ffd6038d6d8abe99", size = 249726 }, + { url = "https://files.pythonhosted.org/packages/a3/85/eebcaa0edafe427e93286b94f56ea7e1280f2c49da0a776a6f37e04481f9/coverage-7.10.5-cp314-cp314-win32.whl", hash = "sha256:556d23d4e6393ca898b2e63a5bca91e9ac2d5fb13299ec286cd69a09a7187fde", size = 219825 }, + { url = "https://files.pythonhosted.org/packages/3c/f7/6d43e037820742603f1e855feb23463979bf40bd27d0cde1f761dcc66a3e/coverage-7.10.5-cp314-cp314-win_amd64.whl", hash = "sha256:f4446a9547681533c8fa3e3c6cf62121eeee616e6a92bd9201c6edd91beffe13", size = 220618 }, + { url = "https://files.pythonhosted.org/packages/4a/b0/ed9432e41424c51509d1da603b0393404b828906236fb87e2c8482a93468/coverage-7.10.5-cp314-cp314-win_arm64.whl", hash = "sha256:5e78bd9cf65da4c303bf663de0d73bf69f81e878bf72a94e9af67137c69b9fe9", size = 219199 }, + { url = "https://files.pythonhosted.org/packages/2f/54/5a7ecfa77910f22b659c820f67c16fc1e149ed132ad7117f0364679a8fa9/coverage-7.10.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:5661bf987d91ec756a47c7e5df4fbcb949f39e32f9334ccd3f43233bbb65e508", size = 217833 }, + { url = "https://files.pythonhosted.org/packages/4e/0e/25672d917cc57857d40edf38f0b867fb9627115294e4f92c8fcbbc18598d/coverage-7.10.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a46473129244db42a720439a26984f8c6f834762fc4573616c1f37f13994b357", size = 218048 }, + { url = "https://files.pythonhosted.org/packages/cb/7c/0b2b4f1c6f71885d4d4b2b8608dcfc79057adb7da4143eb17d6260389e42/coverage-7.10.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1f64b8d3415d60f24b058b58d859e9512624bdfa57a2d1f8aff93c1ec45c429b", size = 259549 }, + { url = "https://files.pythonhosted.org/packages/94/73/abb8dab1609abec7308d83c6aec547944070526578ee6c833d2da9a0ad42/coverage-7.10.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:44d43de99a9d90b20e0163f9770542357f58860a26e24dc1d924643bd6aa7cb4", size = 261715 }, + { url = "https://files.pythonhosted.org/packages/0b/d1/abf31de21ec92731445606b8d5e6fa5144653c2788758fcf1f47adb7159a/coverage-7.10.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a931a87e5ddb6b6404e65443b742cb1c14959622777f2a4efd81fba84f5d91ba", size = 263969 }, + { url = "https://files.pythonhosted.org/packages/9c/b3/ef274927f4ebede96056173b620db649cc9cb746c61ffc467946b9d0bc67/coverage-7.10.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f9559b906a100029274448f4c8b8b0a127daa4dade5661dfd821b8c188058842", size = 261408 }, + { url = "https://files.pythonhosted.org/packages/20/fc/83ca2812be616d69b4cdd4e0c62a7bc526d56875e68fd0f79d47c7923584/coverage-7.10.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b08801e25e3b4526ef9ced1aa29344131a8f5213c60c03c18fe4c6170ffa2874", size = 259168 }, + { url = "https://files.pythonhosted.org/packages/fc/4f/e0779e5716f72d5c9962e709d09815d02b3b54724e38567308304c3fc9df/coverage-7.10.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ed9749bb8eda35f8b636fb7632f1c62f735a236a5d4edadd8bbcc5ea0542e732", size = 260317 }, + { url = "https://files.pythonhosted.org/packages/2b/fe/4247e732f2234bb5eb9984a0888a70980d681f03cbf433ba7b48f08ca5d5/coverage-7.10.5-cp314-cp314t-win32.whl", hash = "sha256:609b60d123fc2cc63ccee6d17e4676699075db72d14ac3c107cc4976d516f2df", size = 220600 }, + { url = "https://files.pythonhosted.org/packages/a7/a0/f294cff6d1034b87839987e5b6ac7385bec599c44d08e0857ac7f164ad0c/coverage-7.10.5-cp314-cp314t-win_amd64.whl", hash = "sha256:0666cf3d2c1626b5a3463fd5b05f5e21f99e6aec40a3192eee4d07a15970b07f", size = 221714 }, + { url = "https://files.pythonhosted.org/packages/23/18/fa1afdc60b5528d17416df440bcbd8fd12da12bfea9da5b6ae0f7a37d0f7/coverage-7.10.5-cp314-cp314t-win_arm64.whl", hash = "sha256:bc85eb2d35e760120540afddd3044a5bf69118a91a296a8b3940dfc4fdcfe1e2", size = 219735 }, + { url = "https://files.pythonhosted.org/packages/08/b6/fff6609354deba9aeec466e4bcaeb9d1ed3e5d60b14b57df2a36fb2273f2/coverage-7.10.5-py3-none-any.whl", hash = "sha256:0be24d35e4db1d23d0db5c0f6a74a962e2ec83c426b5cac09f4234aadef38e4a", size = 208736 }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "cryptography" +version = "45.0.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d6/0d/d13399c94234ee8f3df384819dc67e0c5ce215fb751d567a55a1f4b028c7/cryptography-45.0.6.tar.gz", hash = "sha256:5c966c732cf6e4a276ce83b6e4c729edda2df6929083a952cc7da973c539c719", size = 744949 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/29/2793d178d0eda1ca4a09a7c4e09a5185e75738cc6d526433e8663b460ea6/cryptography-45.0.6-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:048e7ad9e08cf4c0ab07ff7f36cc3115924e22e2266e034450a890d9e312dd74", size = 7042702 }, + { url = "https://files.pythonhosted.org/packages/b3/b6/cabd07410f222f32c8d55486c464f432808abaa1f12af9afcbe8f2f19030/cryptography-45.0.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:44647c5d796f5fc042bbc6d61307d04bf29bccb74d188f18051b635f20a9c75f", size = 4206483 }, + { url = "https://files.pythonhosted.org/packages/8b/9e/f9c7d36a38b1cfeb1cc74849aabe9bf817990f7603ff6eb485e0d70e0b27/cryptography-45.0.6-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e40b80ecf35ec265c452eea0ba94c9587ca763e739b8e559c128d23bff7ebbbf", size = 4429679 }, + { url = "https://files.pythonhosted.org/packages/9c/2a/4434c17eb32ef30b254b9e8b9830cee4e516f08b47fdd291c5b1255b8101/cryptography-45.0.6-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:00e8724bdad672d75e6f069b27970883179bd472cd24a63f6e620ca7e41cc0c5", size = 4210553 }, + { url = "https://files.pythonhosted.org/packages/ef/1d/09a5df8e0c4b7970f5d1f3aff1b640df6d4be28a64cae970d56c6cf1c772/cryptography-45.0.6-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a3085d1b319d35296176af31c90338eeb2ddac8104661df79f80e1d9787b8b2", size = 3894499 }, + { url = "https://files.pythonhosted.org/packages/79/62/120842ab20d9150a9d3a6bdc07fe2870384e82f5266d41c53b08a3a96b34/cryptography-45.0.6-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1b7fa6a1c1188c7ee32e47590d16a5a0646270921f8020efc9a511648e1b2e08", size = 4458484 }, + { url = "https://files.pythonhosted.org/packages/fd/80/1bc3634d45ddfed0871bfba52cf8f1ad724761662a0c792b97a951fb1b30/cryptography-45.0.6-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:275ba5cc0d9e320cd70f8e7b96d9e59903c815ca579ab96c1e37278d231fc402", size = 4210281 }, + { url = "https://files.pythonhosted.org/packages/7d/fe/ffb12c2d83d0ee625f124880a1f023b5878f79da92e64c37962bbbe35f3f/cryptography-45.0.6-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f4028f29a9f38a2025abedb2e409973709c660d44319c61762202206ed577c42", size = 4456890 }, + { url = "https://files.pythonhosted.org/packages/8c/8e/b3f3fe0dc82c77a0deb5f493b23311e09193f2268b77196ec0f7a36e3f3e/cryptography-45.0.6-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ee411a1b977f40bd075392c80c10b58025ee5c6b47a822a33c1198598a7a5f05", size = 4333247 }, + { url = "https://files.pythonhosted.org/packages/b3/a6/c3ef2ab9e334da27a1d7b56af4a2417d77e7806b2e0f90d6267ce120d2e4/cryptography-45.0.6-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e2a21a8eda2d86bb604934b6b37691585bd095c1f788530c1fcefc53a82b3453", size = 4565045 }, + { url = "https://files.pythonhosted.org/packages/31/c3/77722446b13fa71dddd820a5faab4ce6db49e7e0bf8312ef4192a3f78e2f/cryptography-45.0.6-cp311-abi3-win32.whl", hash = "sha256:d063341378d7ee9c91f9d23b431a3502fc8bfacd54ef0a27baa72a0843b29159", size = 2928923 }, + { url = "https://files.pythonhosted.org/packages/38/63/a025c3225188a811b82932a4dcc8457a26c3729d81578ccecbcce2cb784e/cryptography-45.0.6-cp311-abi3-win_amd64.whl", hash = "sha256:833dc32dfc1e39b7376a87b9a6a4288a10aae234631268486558920029b086ec", size = 3403805 }, + { url = "https://files.pythonhosted.org/packages/5b/af/bcfbea93a30809f126d51c074ee0fac5bd9d57d068edf56c2a73abedbea4/cryptography-45.0.6-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:3436128a60a5e5490603ab2adbabc8763613f638513ffa7d311c900a8349a2a0", size = 7020111 }, + { url = "https://files.pythonhosted.org/packages/98/c6/ea5173689e014f1a8470899cd5beeb358e22bb3cf5a876060f9d1ca78af4/cryptography-45.0.6-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0d9ef57b6768d9fa58e92f4947cea96ade1233c0e236db22ba44748ffedca394", size = 4198169 }, + { url = "https://files.pythonhosted.org/packages/ba/73/b12995edc0c7e2311ffb57ebd3b351f6b268fed37d93bfc6f9856e01c473/cryptography-45.0.6-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea3c42f2016a5bbf71825537c2ad753f2870191134933196bee408aac397b3d9", size = 4421273 }, + { url = "https://files.pythonhosted.org/packages/f7/6e/286894f6f71926bc0da67408c853dd9ba953f662dcb70993a59fd499f111/cryptography-45.0.6-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:20ae4906a13716139d6d762ceb3e0e7e110f7955f3bc3876e3a07f5daadec5f3", size = 4199211 }, + { url = "https://files.pythonhosted.org/packages/de/34/a7f55e39b9623c5cb571d77a6a90387fe557908ffc44f6872f26ca8ae270/cryptography-45.0.6-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dac5ec199038b8e131365e2324c03d20e97fe214af051d20c49db129844e8b3", size = 3883732 }, + { url = "https://files.pythonhosted.org/packages/f9/b9/c6d32edbcba0cd9f5df90f29ed46a65c4631c4fbe11187feb9169c6ff506/cryptography-45.0.6-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:18f878a34b90d688982e43f4b700408b478102dd58b3e39de21b5ebf6509c301", size = 4450655 }, + { url = "https://files.pythonhosted.org/packages/77/2d/09b097adfdee0227cfd4c699b3375a842080f065bab9014248933497c3f9/cryptography-45.0.6-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5bd6020c80c5b2b2242d6c48487d7b85700f5e0038e67b29d706f98440d66eb5", size = 4198956 }, + { url = "https://files.pythonhosted.org/packages/55/66/061ec6689207d54effdff535bbdf85cc380d32dd5377173085812565cf38/cryptography-45.0.6-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:eccddbd986e43014263eda489abbddfbc287af5cddfd690477993dbb31e31016", size = 4449859 }, + { url = "https://files.pythonhosted.org/packages/41/ff/e7d5a2ad2d035e5a2af116e1a3adb4d8fcd0be92a18032917a089c6e5028/cryptography-45.0.6-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:550ae02148206beb722cfe4ef0933f9352bab26b087af00e48fdfb9ade35c5b3", size = 4320254 }, + { url = "https://files.pythonhosted.org/packages/82/27/092d311af22095d288f4db89fcaebadfb2f28944f3d790a4cf51fe5ddaeb/cryptography-45.0.6-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5b64e668fc3528e77efa51ca70fadcd6610e8ab231e3e06ae2bab3b31c2b8ed9", size = 4554815 }, + { url = "https://files.pythonhosted.org/packages/7e/01/aa2f4940262d588a8fdf4edabe4cda45854d00ebc6eaac12568b3a491a16/cryptography-45.0.6-cp37-abi3-win32.whl", hash = "sha256:780c40fb751c7d2b0c6786ceee6b6f871e86e8718a8ff4bc35073ac353c7cd02", size = 2912147 }, + { url = "https://files.pythonhosted.org/packages/0a/bc/16e0276078c2de3ceef6b5a34b965f4436215efac45313df90d55f0ba2d2/cryptography-45.0.6-cp37-abi3-win_amd64.whl", hash = "sha256:20d15aed3ee522faac1a39fbfdfee25d17b1284bafd808e1640a74846d7c4d1b", size = 3390459 }, + { url = "https://files.pythonhosted.org/packages/56/d2/4482d97c948c029be08cb29854a91bd2ae8da7eb9c4152461f1244dcea70/cryptography-45.0.6-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:705bb7c7ecc3d79a50f236adda12ca331c8e7ecfbea51edd931ce5a7a7c4f012", size = 3576812 }, + { url = "https://files.pythonhosted.org/packages/ec/24/55fc238fcaa122855442604b8badb2d442367dfbd5a7ca4bb0bd346e263a/cryptography-45.0.6-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:826b46dae41a1155a0c0e66fafba43d0ede1dc16570b95e40c4d83bfcf0a451d", size = 4141694 }, + { url = "https://files.pythonhosted.org/packages/f9/7e/3ea4fa6fbe51baf3903806a0241c666b04c73d2358a3ecce09ebee8b9622/cryptography-45.0.6-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:cc4d66f5dc4dc37b89cfef1bd5044387f7a1f6f0abb490815628501909332d5d", size = 4375010 }, + { url = "https://files.pythonhosted.org/packages/50/42/ec5a892d82d2a2c29f80fc19ced4ba669bca29f032faf6989609cff1f8dc/cryptography-45.0.6-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:f68f833a9d445cc49f01097d95c83a850795921b3f7cc6488731e69bde3288da", size = 4141377 }, + { url = "https://files.pythonhosted.org/packages/e7/d7/246c4c973a22b9c2931999da953a2c19cae7c66b9154c2d62ffed811225e/cryptography-45.0.6-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:3b5bf5267e98661b9b888a9250d05b063220dfa917a8203744454573c7eb79db", size = 4374609 }, + { url = "https://files.pythonhosted.org/packages/78/6d/c49ccf243f0a1b0781c2a8de8123ee552f0c8a417c6367a24d2ecb7c11b3/cryptography-45.0.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2384f2ab18d9be88a6e4f8972923405e2dbb8d3e16c6b43f15ca491d7831bd18", size = 3322156 }, + { url = "https://files.pythonhosted.org/packages/61/69/c252de4ec047ba2f567ecb53149410219577d408c2aea9c989acae7eafce/cryptography-45.0.6-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:fc022c1fa5acff6def2fc6d7819bbbd31ccddfe67d075331a65d9cfb28a20983", size = 3584669 }, + { url = "https://files.pythonhosted.org/packages/e3/fe/deea71e9f310a31fe0a6bfee670955152128d309ea2d1c79e2a5ae0f0401/cryptography-45.0.6-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:3de77e4df42ac8d4e4d6cdb342d989803ad37707cf8f3fbf7b088c9cbdd46427", size = 4153022 }, + { url = "https://files.pythonhosted.org/packages/60/45/a77452f5e49cb580feedba6606d66ae7b82c128947aa754533b3d1bd44b0/cryptography-45.0.6-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:599c8d7df950aa68baa7e98f7b73f4f414c9f02d0e8104a30c0182a07732638b", size = 4386802 }, + { url = "https://files.pythonhosted.org/packages/a3/b9/a2f747d2acd5e3075fdf5c145c7c3568895daaa38b3b0c960ef830db6cdc/cryptography-45.0.6-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:31a2b9a10530a1cb04ffd6aa1cd4d3be9ed49f7d77a4dafe198f3b382f41545c", size = 4152706 }, + { url = "https://files.pythonhosted.org/packages/81/ec/381b3e8d0685a3f3f304a382aa3dfce36af2d76467da0fd4bb21ddccc7b2/cryptography-45.0.6-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:e5b3dda1b00fb41da3af4c5ef3f922a200e33ee5ba0f0bc9ecf0b0c173958385", size = 4386740 }, + { url = "https://files.pythonhosted.org/packages/0a/76/cf8d69da8d0b5ecb0db406f24a63a3f69ba5e791a11b782aeeefef27ccbb/cryptography-45.0.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:629127cfdcdc6806dfe234734d7cb8ac54edaf572148274fa377a7d3405b0043", size = 3331874 }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674 }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 }, +] + +[[package]] +name = "mock" +version = "5.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/07/8c/14c2ae915e5f9dca5a22edd68b35be94400719ccfa068a03e0fb63d0f6f6/mock-5.2.0.tar.gz", hash = "sha256:4e460e818629b4b173f32d08bf30d3af8123afbb8e04bb5707a1fd4799e503f0", size = 92796 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/d9/617e6af809bf3a1d468e0d58c3997b1dc219a9a9202e650d30c2fc85d481/mock-5.2.0-py3-none-any.whl", hash = "sha256:7ba87f72ca0e915175596069dbbcc7c75af7b5e9b9bc107ad6349ede0819982f", size = 31617 }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 }, +] + +[[package]] +name = "pyasn1" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135 }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259 }, +] + +[[package]] +name = "pycparser" +version = "2.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217 }, +] + +[[package]] +name = "pykiteconnect" +version = "5.0.1" +source = { virtual = "." } +dependencies = [ + { name = "pyopenssl" }, + { name = "python-dateutil" }, + { name = "requests" }, + { name = "service-identity" }, + { name = "six" }, + { name = "websockets" }, +] + +[package.dev-dependencies] +dev = [ + { name = "mock" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "responses" }, + { name = "ruff" }, + { name = "urllib3" }, +] + +[package.metadata] +requires-dist = [ + { name = "pyopenssl", specifier = ">=25.1.0" }, + { name = "python-dateutil", specifier = ">=2.9.0.post0" }, + { name = "requests", specifier = ">=2.32.5" }, + { name = "service-identity", specifier = ">=18.1.0" }, + { name = "six", specifier = ">=1.17.0" }, + { name = "websockets", specifier = ">=15.0.1" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "mock", specifier = ">=5.2.0" }, + { name = "pytest", specifier = ">=8.4.1" }, + { name = "pytest-cov", specifier = ">=6.2.1" }, + { name = "responses", specifier = ">=0.25.8" }, + { name = "ruff", specifier = ">=0.12.10" }, + { name = "urllib3", specifier = ">=2.5.0" }, +] + +[[package]] +name = "pyopenssl" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/8c/cd89ad05804f8e3c17dea8f178c3f40eeab5694c30e0c9f5bcd49f576fc3/pyopenssl-25.1.0.tar.gz", hash = "sha256:8d031884482e0c67ee92bf9a4d8cceb08d92aba7136432ffb0703c5280fc205b", size = 179937 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/28/2659c02301b9500751f8d42f9a6632e1508aa5120de5e43042b8b30f8d5d/pyopenssl-25.1.0-py3-none-any.whl", hash = "sha256:2b11f239acc47ac2e5aca04fd7fa829800aeee22a2eb30d744572a157bd8a1ab", size = 56771 }, +] + +[[package]] +name = "pytest" +version = "8.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474 }, +] + +[[package]] +name = "pytest-cov" +version = "6.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/99/668cade231f434aaa59bbfbf49469068d2ddd945000621d3d165d2e7dd7b/pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2", size = 69432 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/16/4ea354101abb1287856baa4af2732be351c7bee728065aed451b678153fd/pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5", size = 24644 }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199 }, + { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758 }, + { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463 }, + { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280 }, + { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239 }, + { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802 }, + { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527 }, + { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052 }, + { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774 }, + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738 }, +] + +[[package]] +name = "responses" +version = "0.25.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, + { name = "requests" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/95/89c054ad70bfef6da605338b009b2e283485835351a9935c7bfbfaca7ffc/responses-0.25.8.tar.gz", hash = "sha256:9374d047a575c8f781b94454db5cab590b6029505f488d12899ddb10a4af1cf4", size = 79320 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/4c/cc276ce57e572c102d9542d383b2cfd551276581dc60004cb94fe8774c11/responses-0.25.8-py3-none-any.whl", hash = "sha256:0c710af92def29c8352ceadff0c3fe340ace27cf5af1bbe46fb71275bcd2831c", size = 34769 }, +] + +[[package]] +name = "ruff" +version = "0.12.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/eb/8c073deb376e46ae767f4961390d17545e8535921d2f65101720ed8bd434/ruff-0.12.10.tar.gz", hash = "sha256:189ab65149d11ea69a2d775343adf5f49bb2426fc4780f65ee33b423ad2e47f9", size = 5310076 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/e7/560d049d15585d6c201f9eeacd2fd130def3741323e5ccf123786e0e3c95/ruff-0.12.10-py3-none-linux_armv6l.whl", hash = "sha256:8b593cb0fb55cc8692dac7b06deb29afda78c721c7ccfed22db941201b7b8f7b", size = 11935161 }, + { url = "https://files.pythonhosted.org/packages/d1/b0/ad2464922a1113c365d12b8f80ed70fcfb39764288ac77c995156080488d/ruff-0.12.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ebb7333a45d56efc7c110a46a69a1b32365d5c5161e7244aaf3aa20ce62399c1", size = 12660884 }, + { url = "https://files.pythonhosted.org/packages/d7/f1/97f509b4108d7bae16c48389f54f005b62ce86712120fd8b2d8e88a7cb49/ruff-0.12.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d59e58586829f8e4a9920788f6efba97a13d1fa320b047814e8afede381c6839", size = 11872754 }, + { url = "https://files.pythonhosted.org/packages/12/ad/44f606d243f744a75adc432275217296095101f83f966842063d78eee2d3/ruff-0.12.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:822d9677b560f1fdeab69b89d1f444bf5459da4aa04e06e766cf0121771ab844", size = 12092276 }, + { url = "https://files.pythonhosted.org/packages/06/1f/ed6c265e199568010197909b25c896d66e4ef2c5e1c3808caf461f6f3579/ruff-0.12.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:37b4a64f4062a50c75019c61c7017ff598cb444984b638511f48539d3a1c98db", size = 11734700 }, + { url = "https://files.pythonhosted.org/packages/63/c5/b21cde720f54a1d1db71538c0bc9b73dee4b563a7dd7d2e404914904d7f5/ruff-0.12.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c6f4064c69d2542029b2a61d39920c85240c39837599d7f2e32e80d36401d6e", size = 13468783 }, + { url = "https://files.pythonhosted.org/packages/02/9e/39369e6ac7f2a1848f22fb0b00b690492f20811a1ac5c1fd1d2798329263/ruff-0.12.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:059e863ea3a9ade41407ad71c1de2badfbe01539117f38f763ba42a1206f7559", size = 14436642 }, + { url = "https://files.pythonhosted.org/packages/e3/03/5da8cad4b0d5242a936eb203b58318016db44f5c5d351b07e3f5e211bb89/ruff-0.12.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1bef6161e297c68908b7218fa6e0e93e99a286e5ed9653d4be71e687dff101cf", size = 13859107 }, + { url = "https://files.pythonhosted.org/packages/19/19/dd7273b69bf7f93a070c9cec9494a94048325ad18fdcf50114f07e6bf417/ruff-0.12.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4f1345fbf8fb0531cd722285b5f15af49b2932742fc96b633e883da8d841896b", size = 12886521 }, + { url = "https://files.pythonhosted.org/packages/c0/1d/b4207ec35e7babaee62c462769e77457e26eb853fbdc877af29417033333/ruff-0.12.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f68433c4fbc63efbfa3ba5db31727db229fa4e61000f452c540474b03de52a9", size = 13097528 }, + { url = "https://files.pythonhosted.org/packages/ff/00/58f7b873b21114456e880b75176af3490d7a2836033779ca42f50de3b47a/ruff-0.12.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:141ce3d88803c625257b8a6debf4a0473eb6eed9643a6189b68838b43e78165a", size = 13080443 }, + { url = "https://files.pythonhosted.org/packages/12/8c/9e6660007fb10189ccb78a02b41691288038e51e4788bf49b0a60f740604/ruff-0.12.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f3fc21178cd44c98142ae7590f42ddcb587b8e09a3b849cbc84edb62ee95de60", size = 11896759 }, + { url = "https://files.pythonhosted.org/packages/67/4c/6d092bb99ea9ea6ebda817a0e7ad886f42a58b4501a7e27cd97371d0ba54/ruff-0.12.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7d1a4e0bdfafcd2e3e235ecf50bf0176f74dd37902f241588ae1f6c827a36c56", size = 11701463 }, + { url = "https://files.pythonhosted.org/packages/59/80/d982c55e91df981f3ab62559371380616c57ffd0172d96850280c2b04fa8/ruff-0.12.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:e67d96827854f50b9e3e8327b031647e7bcc090dbe7bb11101a81a3a2cbf1cc9", size = 12691603 }, + { url = "https://files.pythonhosted.org/packages/ad/37/63a9c788bbe0b0850611669ec6b8589838faf2f4f959647f2d3e320383ae/ruff-0.12.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ae479e1a18b439c59138f066ae79cc0f3ee250712a873d00dbafadaad9481e5b", size = 13164356 }, + { url = "https://files.pythonhosted.org/packages/47/d4/1aaa7fb201a74181989970ebccd12f88c0fc074777027e2a21de5a90657e/ruff-0.12.10-py3-none-win32.whl", hash = "sha256:9de785e95dc2f09846c5e6e1d3a3d32ecd0b283a979898ad427a9be7be22b266", size = 11896089 }, + { url = "https://files.pythonhosted.org/packages/ad/14/2ad38fd4037daab9e023456a4a40ed0154e9971f8d6aed41bdea390aabd9/ruff-0.12.10-py3-none-win_amd64.whl", hash = "sha256:7837eca8787f076f67aba2ca559cefd9c5cbc3a9852fd66186f4201b87c1563e", size = 13004616 }, + { url = "https://files.pythonhosted.org/packages/24/3c/21cf283d67af33a8e6ed242396863af195a8a6134ec581524fd22b9811b6/ruff-0.12.10-py3-none-win_arm64.whl", hash = "sha256:cc138cc06ed9d4bfa9d667a65af7172b47840e1a98b02ce7011c391e54635ffc", size = 12074225 }, +] + +[[package]] +name = "service-identity" +version = "24.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "cryptography" }, + { name = "pyasn1" }, + { name = "pyasn1-modules" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/a5/dfc752b979067947261dbbf2543470c58efe735c3c1301dd870ef27830ee/service_identity-24.2.0.tar.gz", hash = "sha256:b8683ba13f0d39c6cd5d625d2c5f65421d6d707b013b375c355751557cbe8e09", size = 39245 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/2c/ca6dd598b384bc1ce581e24aaae0f2bed4ccac57749d5c3befbb5e742081/service_identity-24.2.0-py3-none-any.whl", hash = "sha256:6b047fbd8a84fd0bb0d55ebce4031e400562b9196e1e0d3e0fe2b8a59f6d4a85", size = 11364 }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, +] + +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, +] + +[[package]] +name = "typing-extensions" +version = "4.14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906 }, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795 }, +] + +[[package]] +name = "websockets" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/da/6462a9f510c0c49837bbc9345aca92d767a56c1fb2939e1579df1e1cdcf7/websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b", size = 175423 }, + { url = "https://files.pythonhosted.org/packages/1c/9f/9d11c1a4eb046a9e106483b9ff69bce7ac880443f00e5ce64261b47b07e7/websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205", size = 173080 }, + { url = "https://files.pythonhosted.org/packages/d5/4f/b462242432d93ea45f297b6179c7333dd0402b855a912a04e7fc61c0d71f/websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a", size = 173329 }, + { url = "https://files.pythonhosted.org/packages/6e/0c/6afa1f4644d7ed50284ac59cc70ef8abd44ccf7d45850d989ea7310538d0/websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e", size = 182312 }, + { url = "https://files.pythonhosted.org/packages/dd/d4/ffc8bd1350b229ca7a4db2a3e1c482cf87cea1baccd0ef3e72bc720caeec/websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf", size = 181319 }, + { url = "https://files.pythonhosted.org/packages/97/3a/5323a6bb94917af13bbb34009fac01e55c51dfde354f63692bf2533ffbc2/websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb", size = 181631 }, + { url = "https://files.pythonhosted.org/packages/a6/cc/1aeb0f7cee59ef065724041bb7ed667b6ab1eeffe5141696cccec2687b66/websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d", size = 182016 }, + { url = "https://files.pythonhosted.org/packages/79/f9/c86f8f7af208e4161a7f7e02774e9d0a81c632ae76db2ff22549e1718a51/websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9", size = 181426 }, + { url = "https://files.pythonhosted.org/packages/c7/b9/828b0bc6753db905b91df6ae477c0b14a141090df64fb17f8a9d7e3516cf/websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c", size = 181360 }, + { url = "https://files.pythonhosted.org/packages/89/fb/250f5533ec468ba6327055b7d98b9df056fb1ce623b8b6aaafb30b55d02e/websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256", size = 176388 }, + { url = "https://files.pythonhosted.org/packages/1c/46/aca7082012768bb98e5608f01658ff3ac8437e563eca41cf068bd5849a5e/websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41", size = 176830 }, + { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423 }, + { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082 }, + { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330 }, + { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878 }, + { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883 }, + { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252 }, + { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521 }, + { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958 }, + { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918 }, + { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388 }, + { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828 }, + { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437 }, + { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096 }, + { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332 }, + { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152 }, + { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096 }, + { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523 }, + { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790 }, + { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165 }, + { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160 }, + { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395 }, + { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841 }, + { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440 }, + { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098 }, + { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329 }, + { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111 }, + { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054 }, + { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496 }, + { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829 }, + { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217 }, + { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195 }, + { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393 }, + { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837 }, + { url = "https://files.pythonhosted.org/packages/02/9e/d40f779fa16f74d3468357197af8d6ad07e7c5a27ea1ca74ceb38986f77a/websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3", size = 173109 }, + { url = "https://files.pythonhosted.org/packages/bc/cd/5b887b8585a593073fd92f7c23ecd3985cd2c3175025a91b0d69b0551372/websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1", size = 173343 }, + { url = "https://files.pythonhosted.org/packages/fe/ae/d34f7556890341e900a95acf4886833646306269f899d58ad62f588bf410/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475", size = 174599 }, + { url = "https://files.pythonhosted.org/packages/71/e6/5fd43993a87db364ec60fc1d608273a1a465c0caba69176dd160e197ce42/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9", size = 174207 }, + { url = "https://files.pythonhosted.org/packages/2b/fb/c492d6daa5ec067c2988ac80c61359ace5c4c674c532985ac5a123436cec/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04", size = 174155 }, + { url = "https://files.pythonhosted.org/packages/68/a1/dcb68430b1d00b698ae7a7e0194433bce4f07ded185f0ee5fb21e2a2e91e/websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122", size = 176884 }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743 }, +]