From ee8381173721c2c4ea21d7136af8f8352ce03afa Mon Sep 17 00:00:00 2001 From: genaro Date: Mon, 24 Jun 2024 16:54:23 -0300 Subject: [PATCH 1/9] can now get trade history of byma --- src/pyRofex/clients/rest_rfx.py | 14 ++++++++++++-- src/pyRofex/components/enums.py | 2 ++ src/pyRofex/components/urls.py | 1 + 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/pyRofex/clients/rest_rfx.py b/src/pyRofex/clients/rest_rfx.py index e92c03d..bd19174 100644 --- a/src/pyRofex/clients/rest_rfx.py +++ b/src/pyRofex/clients/rest_rfx.py @@ -41,7 +41,7 @@ def __init__(self, environment, active_token=None): self.environment["token"] = active_token self.environment["initialized"] = True - def get_trade_history(self, ticker, start_date, end_date, market): + def get_trade_history(self, ticker, start_date, end_date, market=Market.ROFEX, clearing= None): """Makes a request to the API and get trade history for the instrument. For more detailed information go to: https://apihub.primary.com.ar/assets/docs/Primary-API.pdf @@ -54,13 +54,23 @@ def get_trade_history(self, ticker, start_date, end_date, market): :type end_date: str :param market: Market ID related to the instrument. :type market: Market (Enum). + :param clearing: clearing of the instrument to send in the request to byma exchange, only can be 24hs or CI. + :type clearing: str :return: List of trades returned by the API. :rtype: dict of JSON response. """ - return self.api_request(urls.historic_trades.format(m=market.value, + + if market == Market.ROFEX: + return self.api_request(urls.historic_trades.format(m=market, s=ticker, df=start_date, dt=end_date)) + else: + return self.api_request(urls.historic_trades_byma.format(m=market, + s=ticker, + c=clearing, + df=start_date, + dt=end_date)) def get_segments(self): """Make a request to the API and get a list of valid segments. diff --git a/src/pyRofex/components/enums.py b/src/pyRofex/components/enums.py index 950dddc..4fb807f 100644 --- a/src/pyRofex/components/enums.py +++ b/src/pyRofex/components/enums.py @@ -55,8 +55,10 @@ class TimeInForce(Enum): class Market(Enum): """Market ID associated to the instruments. ROFEX: ROFEX Exchange. + BYMA: BYMA Exchange. """ ROFEX = 'ROFX' + BYMA = "MERV" class MarketSegment(Enum): diff --git a/src/pyRofex/components/urls.py b/src/pyRofex/components/urls.py index f4ea92f..3f7681a 100644 --- a/src/pyRofex/components/urls.py +++ b/src/pyRofex/components/urls.py @@ -16,6 +16,7 @@ "by_segments": "rest/instruments/bySegment?MarketSegmentID={market_segment}&MarketID={market}"} market_data = "rest/marketdata/get?marketId={m}&symbol={s}&entries={e}&depth={d}" historic_trades = "rest/data/getTrades?marketId={m}&symbol={s}&dateFrom={df}&dateTo={dt}" +historic_trades_byma = "rest/data/getTrades?marketId={m}&symbol=MERV%20-%20XMEV%20-%20{s}%20-%20{c}&dateFrom={df}&dateTo={dt}&external=1" order_status = "rest/order/id?clOrdId={c}&proprietary={p}" new_order = "rest/order/newSingleOrder?marketId={market}&symbol={ticker}" \ "&orderQty={size}&ordType={type}&side={side}&timeInForce={time_force}" \ From ad56c973bc650a3250a0019a317507537c909ef1 Mon Sep 17 00:00:00 2001 From: genaro Date: Mon, 24 Jun 2024 23:02:28 -0300 Subject: [PATCH 2/9] added functionality to get historic data from byma --- src/pyRofex/clients/rest_rfx.py | 26 ++++++++++++-------------- src/pyRofex/components/enums.py | 2 +- src/pyRofex/components/urls.py | 3 +-- 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/src/pyRofex/clients/rest_rfx.py b/src/pyRofex/clients/rest_rfx.py index bd19174..d62ae1b 100644 --- a/src/pyRofex/clients/rest_rfx.py +++ b/src/pyRofex/clients/rest_rfx.py @@ -41,12 +41,12 @@ def __init__(self, environment, active_token=None): self.environment["token"] = active_token self.environment["initialized"] = True - def get_trade_history(self, ticker, start_date, end_date, market=Market.ROFEX, clearing= None): + def get_trade_history(self, ticker, start_date, end_date, market=Market.ROFEX): """Makes a request to the API and get trade history for the instrument. For more detailed information go to: https://apihub.primary.com.ar/assets/docs/Primary-API.pdf - :param ticker: Ticker of the instrument to send in the request. Example: DLR/MAR23 + :param ticker: Ticker of the instrument to send in the request. Example: DLR/JUL24 for MtB, MERV - XMEV - GGAL - 24hs for BYMA :type ticker: str :param start_date: Start date for the trades. Format: yyyy-MM-dd :type start_date: str @@ -54,23 +54,21 @@ def get_trade_history(self, ticker, start_date, end_date, market=Market.ROFEX, c :type end_date: str :param market: Market ID related to the instrument. :type market: Market (Enum). - :param clearing: clearing of the instrument to send in the request to byma exchange, only can be 24hs or CI. - :type clearing: str :return: List of trades returned by the API. :rtype: dict of JSON response. """ - - if market == Market.ROFEX: - return self.api_request(urls.historic_trades.format(m=market, - s=ticker, - df=start_date, - dt=end_date)) - else: - return self.api_request(urls.historic_trades_byma.format(m=market, + + external = "" + if market != Market.ROFEX: + ticker = ticker.replace(" ", "%20") + external = "&external=1" + + return self.api_request(urls.historic_trades.format(m=market.value, s=ticker, - c=clearing, df=start_date, - dt=end_date)) + dt=end_date, + ext=external)) + def get_segments(self): """Make a request to the API and get a list of valid segments. diff --git a/src/pyRofex/components/enums.py b/src/pyRofex/components/enums.py index 4fb807f..0b4e8e9 100644 --- a/src/pyRofex/components/enums.py +++ b/src/pyRofex/components/enums.py @@ -58,7 +58,7 @@ class Market(Enum): BYMA: BYMA Exchange. """ ROFEX = 'ROFX' - BYMA = "MERV" + BYMA = 'MERV' class MarketSegment(Enum): diff --git a/src/pyRofex/components/urls.py b/src/pyRofex/components/urls.py index 3f7681a..e1f8539 100644 --- a/src/pyRofex/components/urls.py +++ b/src/pyRofex/components/urls.py @@ -15,8 +15,7 @@ "by_cfi": "rest/instruments/byCFICode?CFICode={cfi_code}", "by_segments": "rest/instruments/bySegment?MarketSegmentID={market_segment}&MarketID={market}"} market_data = "rest/marketdata/get?marketId={m}&symbol={s}&entries={e}&depth={d}" -historic_trades = "rest/data/getTrades?marketId={m}&symbol={s}&dateFrom={df}&dateTo={dt}" -historic_trades_byma = "rest/data/getTrades?marketId={m}&symbol=MERV%20-%20XMEV%20-%20{s}%20-%20{c}&dateFrom={df}&dateTo={dt}&external=1" +historic_trades = "rest/data/getTrades?marketId={m}&symbol={s}&dateFrom={df}&dateTo={dt}{ext}" order_status = "rest/order/id?clOrdId={c}&proprietary={p}" new_order = "rest/order/newSingleOrder?marketId={market}&symbol={ticker}" \ "&orderQty={size}&ordType={type}&side={side}&timeInForce={time_force}" \ From d4b915f41afd59c318ece60e86981b4fc951e248 Mon Sep 17 00:00:00 2001 From: GenaroVogelius Date: Fri, 9 Jan 2026 11:42:03 -0300 Subject: [PATCH 3/9] feat: using pyproject.toml --- pyproject.toml | 46 ++++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 4 ---- setup.py | 35 ++--------------------------------- 3 files changed, 48 insertions(+), 37 deletions(-) create mode 100644 pyproject.toml delete mode 100644 requirements.txt diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..cef3003 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,46 @@ +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "pyRofex fork" +version = "0.5.1" +description = "Python connector for ROFEX's Rest and Websocket APIs." +readme = "README.rst" +requires-python = ">=3.7" +license = {text = "MIT"} +authors = [ + {name = "Genaro Vogelius", email = "genagevo@gmail.com"} +] +classifiers = [ + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Intended Audience :: Information Technology", + "Intended Audience :: Science/Research", + "Topic :: Office/Business :: Financial :: Investment", + "Topic :: Software Development", +] +dependencies = [ + "requests>=2.32.5", + "simplejson>=3.20.0", + "websocket-client>=1.9.0", +] + +[project.urls] +Homepage = "https://github.com/GenaroVogelius/pyRofex" + +[tool.setuptools] +package-dir = {"" = "src"} + +[tool.setuptools.packages.find] +where = ["src"] diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index f0ef099..0000000 --- a/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -requests>=2.20.0 -simplejson>=3.10.0 -enum34>=1.1.6 -websocket-client>=1.6.4 \ No newline at end of file diff --git a/setup.py b/setup.py index e02b51e..6068493 100644 --- a/setup.py +++ b/setup.py @@ -1,34 +1,3 @@ -import setuptools +from setuptools import setup -with open("README.rst", "r") as fh: - long_description = fh.read() - -setuptools.setup( - name="pyRofex", - version="0.5.0", - author="Franco Zanuso", - author_email="francozanuso89@gmail.com", - description="Python connector for ROFEX's Rest and Websocket APIs.", - long_description=long_description, - long_description_content_type="text/x-rst", - url="https://github.com/gruporofex/pyRofex", - packages=setuptools.find_packages(where='src'), - package_dir={'': 'src'}, - install_requires=[ - 'requests>=2.20.0', - 'simplejson>=3.10.0', - 'enum34>=1.1.6', - 'websocket-client>=1.6.4', - ], - classifiers=[ - "Programming Language :: Python :: 3", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - "Development Status :: 4 - Beta", - "Intended Audience :: Developers", - "Intended Audience :: Information Technology", - "Intended Audience :: Science/Research", - "Topic :: Office/Business :: Financial :: Investment", - "Topic :: Software Development" - ], -) \ No newline at end of file +setup() From 952f26e5ac217ccdf0223c0cc1c60fc3ce5cd192 Mon Sep 17 00:00:00 2001 From: GenaroVogelius Date: Fri, 9 Jan 2026 11:46:26 -0300 Subject: [PATCH 4/9] refactor: changed name of project --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index cef3003..b8cfd11 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ requires = ["setuptools>=61.0", "wheel"] build-backend = "setuptools.build_meta" [project] -name = "pyRofex fork" +name = "pyRofex_V2" version = "0.5.1" description = "Python connector for ROFEX's Rest and Websocket APIs." readme = "README.rst" From 046b80d43630b68139c54d925212290ebf90519f Mon Sep 17 00:00:00 2001 From: GenaroVogelius Date: Fri, 9 Jan 2026 11:47:31 -0300 Subject: [PATCH 5/9] refactor: changed name of project to pyRofex --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b8cfd11..4ff7a0e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ requires = ["setuptools>=61.0", "wheel"] build-backend = "setuptools.build_meta" [project] -name = "pyRofex_V2" +name = "pyRofex" version = "0.5.1" description = "Python connector for ROFEX's Rest and Websocket APIs." readme = "README.rst" From e781251afe95c22921808638899ef9a8f31692ae Mon Sep 17 00:00:00 2001 From: GenaroVogelius Date: Fri, 9 Jan 2026 13:00:07 -0300 Subject: [PATCH 6/9] feat: making more easier to e live environment --- src/pyRofex/components/enums.py | 11 +- src/pyRofex/components/urls.py | 2 - src/pyRofex/service.py | 196 ++++++++++++++++++++++---------- 3 files changed, 147 insertions(+), 62 deletions(-) diff --git a/src/pyRofex/components/enums.py b/src/pyRofex/components/enums.py index 0b4e8e9..756a301 100644 --- a/src/pyRofex/components/enums.py +++ b/src/pyRofex/components/enums.py @@ -4,7 +4,8 @@ Defines all library enumerations """ -from enum import Enum +from enum import Enum, StrEnum + class Environment(Enum): @@ -132,3 +133,11 @@ class MarketDataEntry(Enum): NOMINAL_VOLUME = "NV" ACP = "ACP" TRADE_COUNT = "TC" + + +class Broker(StrEnum): + """ + Brokers that have matriz + """ + VETA = "veta" + diff --git a/src/pyRofex/components/urls.py b/src/pyRofex/components/urls.py index e1f8539..72466fe 100644 --- a/src/pyRofex/components/urls.py +++ b/src/pyRofex/components/urls.py @@ -4,8 +4,6 @@ Defines all API Paths """ -from .enums import OrderType -from .enums import TimeInForce auth = "auth/getToken" segments = "rest/segment/all" diff --git a/src/pyRofex/service.py b/src/pyRofex/service.py index 3e93d79..f2be83d 100644 --- a/src/pyRofex/service.py +++ b/src/pyRofex/service.py @@ -1,29 +1,36 @@ # -*- coding: utf-8 -*- """ - pyRofex.service +pyRofex.service - All the library exposed functionality +All the library exposed functionality """ + import logging from inspect import getfullargspec from .clients.rest_rfx import RestClient from .clients.websocket_rfx import WebSocketClient from .components import globals +from .components.enums import Environment, Market, MarketDataEntry, TimeInForce from .components.exceptions import ApiException -from .components.enums import Environment -from .components.enums import MarketDataEntry -from .components.enums import TimeInForce -from .components.enums import Market - +from .components.enums import Broker # ###################################################### # ## Initialization functions ## # ###################################################### -def initialize(user, password, account, environment, proxies=None, ssl_opt=None, active_token=None): - """ Initialize the specified environment. +def initialize( + user, + password, + account, + environment, + proxies=None, + ssl_opt=None, + active_token=None, + broker : Broker = None, +): + """Initialize the specified environment. Set the default user, password and account for the environment. @@ -41,10 +48,18 @@ def initialize(user, password, account, environment, proxies=None, ssl_opt=None, :type ssl_opt: dict :param active_token: (optional) Already Active token. If None, it will request new token on authentication. :type active_token: str + :param broker: the broker name, Ex. "veta". Required when environment is LIVE. + :type broker: str """ _validate_environment(environment) - _set_environment_parameters(user, password, account, environment, proxies, ssl_opt) - globals.environment_config[environment]["rest_client"] = RestClient(environment, active_token) + if environment == Environment.LIVE and broker not in Broker: + raise ApiException("Broker parameter is required when using Environment.LIVE") + _set_environment_parameters( + user, password, account, environment, proxies, ssl_opt, broker + ) + globals.environment_config[environment]["rest_client"] = RestClient( + environment, active_token + ) globals.environment_config[environment]["ws_client"] = WebSocketClient(environment) set_default_environment(environment) @@ -81,7 +96,9 @@ def _set_environment_parameter(parameter, value, environment): globals.environment_config[environment][parameter] = value -def _set_environment_parameters(user, password, account, environment, proxies, ssl_opt): +def _set_environment_parameters( + user, password, account, environment, proxies, ssl_opt, broker +): """Configure the environment parameters into global configuration. Set the user, password and account into globals configuration. @@ -95,6 +112,8 @@ def _set_environment_parameters(user, password, account, environment, proxies, s :type proxies: dict :param ssl_opt: (optional) Dictionary with ssl options for websocket connection. :type ssl_opt: dict + :param broker: the broker name, Ex. "veta". + :type broker: str """ globals.environment_config[environment]["user"] = user globals.environment_config[environment]["password"] = password @@ -102,6 +121,12 @@ def _set_environment_parameters(user, password, account, environment, proxies, s globals.environment_config[environment]["proxies"] = proxies globals.environment_config[environment]["ssl_opt"] = ssl_opt + if environment == Environment.LIVE: + url_template = f"https://api.{broker}.xoms.com.ar/" + ws_template = f"wss://api.{broker}.xoms.com.ar/" + globals.environment_config[environment]["url"] = url_template + globals.environment_config[environment]["ws"] = ws_template + # ###################################################### # ## REST functions ## @@ -128,7 +153,7 @@ def get_segments(environment=None): return response -def get_instruments(endpoint='all', environment=None, **kwargs): +def get_instruments(endpoint="all", environment=None, **kwargs): """Make a request to the API and get the information depending on the given endpoint. Valid 'endpoints' are: 'all', 'details', 'detail', 'by_cfi', 'by_segments'. @@ -216,7 +241,9 @@ def get_instrument_details(ticker, market=Market.ROFEX, environment=None): return client.get_instrument_details(ticker, market) -def get_market_data(ticker, entries=None, depth=1, market=Market.ROFEX, environment=None): +def get_market_data( + ticker, entries=None, depth=1, market=Market.ROFEX, environment=None +): """Make a request to the API to get the Market Data Entries of the specified instrument. For more detailed information go to: https://apihub.primary.com.ar/assets/docs/Primary-API.pdf @@ -274,16 +301,21 @@ def get_order_status(client_order_id, proprietary=None, environment=None): return client.get_order_status(client_order_id, proprietary) -def send_order(ticker, size, order_type, side, - market=Market.ROFEX, - time_in_force=TimeInForce.DAY, - account=None, - price=None, - cancel_previous=False, - iceberg=False, - expire_date=None, - display_quantity=None, - environment=None): +def send_order( + ticker, + size, + order_type, + side, + market=Market.ROFEX, + time_in_force=TimeInForce.DAY, + account=None, + price=None, + cancel_previous=False, + iceberg=False, + expire_date=None, + display_quantity=None, + environment=None, +): """Make a request to the API that send a new order to the Market. For more detailed information go to: https://apihub.primary.com.ar/assets/docs/Primary-API.pdf @@ -330,9 +362,20 @@ def send_order(ticker, size, order_type, side, # Get the client for the environment and make the request client = globals.environment_config[environment]["rest_client"] - return client.send_order(ticker, size, order_type, side, account, - price, time_in_force, market, cancel_previous, - iceberg, expire_date, display_quantity) + return client.send_order( + ticker, + size, + order_type, + side, + account, + price, + time_in_force, + market, + cancel_previous, + iceberg, + expire_date, + display_quantity, + ) def cancel_order(client_order_id, proprietary=None, environment=None): @@ -362,8 +405,7 @@ def cancel_order(client_order_id, proprietary=None, environment=None): # Get the client for the environment and make the request client = globals.environment_config[environment]["rest_client"] - return client.cancel_order(client_order_id, - proprietary) + return client.cancel_order(client_order_id, proprietary) def get_all_orders_status(account=None, environment=None): @@ -393,7 +435,9 @@ def get_all_orders_status(account=None, environment=None): return client.get_all_orders_by_account(account) -def get_trade_history(ticker, start_date, end_date, market=Market.ROFEX, environment=None): +def get_trade_history( + ticker, start_date, end_date, market=Market.ROFEX, environment=None +): """Makes a request to the API and get trade history for the instrument specified. For more detailed information go to: https://apihub.primary.com.ar/assets/docs/Primary-API.pdf @@ -507,11 +551,13 @@ def get_account_report(account=None, environment=None): # ###################################################### -def init_websocket_connection(market_data_handler=None, - order_report_handler=None, - error_handler=None, - exception_handler=None, - environment=None): +def init_websocket_connection( + market_data_handler=None, + order_report_handler=None, + error_handler=None, + exception_handler=None, + environment=None, +): """Initialize the Websocket Client with the handlers and then start the connection with Primary Websocket API. A new thread is created in order to motorize the connection and check new incoming messages. @@ -573,7 +619,9 @@ def close_websocket_connection(environment=None): client.close_connection() -def order_report_subscription(account=None, snapshot=True, handler=None, environment=None): +def order_report_subscription( + account=None, snapshot=True, handler=None, environment=None +): """Send an Order Report Subscription Message through the connection. :param account: account that will be send in the message. @@ -600,14 +648,18 @@ def order_report_subscription(account=None, snapshot=True, handler=None, environ # Checks the handler, then validates and adds it into the client. if handler is not None: _validate_handler(handler) - globals.environment_config[environment]["ws_client"].add_order_report_handler(handler) + globals.environment_config[environment]["ws_client"].add_order_report_handler( + handler + ) # Get the client for the environment and send the subscription message client = globals.environment_config[environment]["ws_client"] client.order_report_subscription(account, snapshot) -def market_data_subscription(tickers, entries, depth=1, market=Market.ROFEX, handler=None, environment=None): +def market_data_subscription( + tickers, entries, depth=1, market=Market.ROFEX, handler=None, environment=None +): """Send a Market Data Subscription Message through the connection. :param tickers: list of the the instruments to be subscribe. @@ -634,7 +686,9 @@ def market_data_subscription(tickers, entries, depth=1, market=Market.ROFEX, han # Checks the handler, then validates and adds it into the client. if handler is not None: _validate_handler(handler) - globals.environment_config[environment]["ws_client"].add_market_data_handler(handler) + globals.environment_config[environment]["ws_client"].add_market_data_handler( + handler + ) # Get the client for the environment and send the subscription message client = globals.environment_config[environment]["ws_client"] @@ -807,18 +861,23 @@ def cancel_order_via_websocket(client_order_id, proprietary=None, environment=No client.cancel_order(client_order_id, proprietary) -def send_order_via_websocket(ticker, size, side, order_type, - all_or_none=False, - market=Market.ROFEX, - time_in_force=TimeInForce.DAY, - account=None, - price=None, - cancel_previous=False, - iceberg=False, - expire_date=None, - display_quantity=None, - environment=None, - ws_client_order_id=None): +def send_order_via_websocket( + ticker, + size, + side, + order_type, + all_or_none=False, + market=Market.ROFEX, + time_in_force=TimeInForce.DAY, + account=None, + price=None, + cancel_previous=False, + iceberg=False, + expire_date=None, + display_quantity=None, + environment=None, + ws_client_order_id=None, +): """Send orders via websocket For more detailed information go to: https://apihub.primary.com.ar/assets/docs/Primary-API.pdf @@ -868,10 +927,22 @@ def send_order_via_websocket(ticker, size, side, order_type, account = globals.environment_config[environment]["account"] # Send the order - client.send_order(ticker, size, side, order_type, account, - price, time_in_force, market, cancel_previous, - iceberg, expire_date, display_quantity, - all_or_none, ws_client_order_id) + client.send_order( + ticker, + size, + side, + order_type, + account, + price, + time_in_force, + market, + cancel_previous, + iceberg, + expire_date, + display_quantity, + all_or_none, + ws_client_order_id, + ) # ###################################################### @@ -890,7 +961,10 @@ def _validate_parameter(parameter, environment): :type environment: Environment (Enum). """ if parameter not in globals.environment_config[environment]: - raise ApiException("Invalid parameter '%s' for the environment %s." % (parameter, environment.name)) + raise ApiException( + "Invalid parameter '%s' for the environment %s." + % (parameter, environment.name) + ) def _validate_environment(environment): @@ -953,7 +1027,7 @@ def _validate_account(account, environment): def _validate_handler(handler): - """ Checks if the handler is callable and that can received one argument, if not raised an ApiException. + """Checks if the handler is callable and that can received one argument, if not raised an ApiException. :param handler: handler to be validated. :type handler: callable. @@ -961,12 +1035,16 @@ def _validate_handler(handler): # Checks if it is callable if not callable(handler): - raise ApiException("Handler '{handler}' is not callable.".format(handler=handler)) + raise ApiException( + "Handler '{handler}' is not callable.".format(handler=handler) + ) # Checks if function can receive an argument fun_arg_spec = getfullargspec(handler) if not fun_arg_spec.args and not fun_arg_spec.varargs: - logging.error("Handler '{handler}' can't receive an argument.".format(handler=handler)) + logging.error( + "Handler '{handler}' can't receive an argument.".format(handler=handler) + ) def _validate_market_data_entries(entries): From fe2f40746b1a2d62e9d4ec2bfdf19a66c4078169 Mon Sep 17 00:00:00 2001 From: GenaroVogelius Date: Mon, 12 Jan 2026 19:53:34 -0300 Subject: [PATCH 7/9] feat: making async all http requests --- pyproject.toml | 6 +- src/pyRofex/clients/rest_rfx.py | 220 ++++++++++++++++++-------------- src/pyRofex/service.py | 114 +++++++++-------- uv.lock | 193 ++++++++++++++++++++++++++++ 4 files changed, 379 insertions(+), 154 deletions(-) create mode 100644 uv.lock diff --git a/pyproject.toml b/pyproject.toml index 4ff7a0e..d8c369c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,15 +7,13 @@ name = "pyRofex" version = "0.5.1" description = "Python connector for ROFEX's Rest and Websocket APIs." readme = "README.rst" -requires-python = ">=3.7" +requires-python = ">=3.9" license = {text = "MIT"} authors = [ {name = "Genaro Vogelius", email = "genagevo@gmail.com"} ] classifiers = [ "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", @@ -31,7 +29,7 @@ classifiers = [ "Topic :: Software Development", ] dependencies = [ - "requests>=2.32.5", + "httpx>=0.28.1", "simplejson>=3.20.0", "websocket-client>=1.9.0", ] diff --git a/src/pyRofex/clients/rest_rfx.py b/src/pyRofex/clients/rest_rfx.py index d62ae1b..6d8ff51 100644 --- a/src/pyRofex/clients/rest_rfx.py +++ b/src/pyRofex/clients/rest_rfx.py @@ -1,25 +1,23 @@ # -*- coding: utf-8 -*- """ - pyRofex.rest_client +pyRofex.rest_client - Defines a Rest Client that implements ROFEX Rest API. +Defines a Rest Client that implements ROFEX Rest API. """ + +import asyncio import re -import requests + +import httpx import simplejson -from ..components import urls -from ..components import globals -from ..components.enums import Market -from ..components.enums import CFICode -from ..components.enums import OrderType -from ..components.enums import TimeInForce -from ..components.enums import MarketSegment +from ..components import globals, urls +from ..components.enums import CFICode, Market, MarketSegment, OrderType, TimeInForce from ..components.exceptions import ApiException class RestClient: - """ Rest Client that implements call to ROFEX REST API. + """Rest Client that implements call to ROFEX REST API. For more information about the API go to: https://apihub.primary.com.ar/assets/docs/Primary-API.pdf """ @@ -29,19 +27,21 @@ def __init__(self, environment, active_token=None): :param environment: the environment that will be associated with the client. :type environment: Environment (Enum) - """ + """ # Environment associated with Client self.environment = globals.environment_config[environment] # Get the authentication Token. if not active_token: - self.update_token() + asyncio.run(self.update_token()) else: self.environment["token"] = active_token self.environment["initialized"] = True - def get_trade_history(self, ticker, start_date, end_date, market=Market.ROFEX): + async def get_trade_history( + self, ticker, start_date, end_date, market=Market.ROFEX + ): """Makes a request to the API and get trade history for the instrument. For more detailed information go to: https://apihub.primary.com.ar/assets/docs/Primary-API.pdf @@ -63,14 +63,13 @@ def get_trade_history(self, ticker, start_date, end_date, market=Market.ROFEX): ticker = ticker.replace(" ", "%20") external = "&external=1" - return self.api_request(urls.historic_trades.format(m=market.value, - s=ticker, - df=start_date, - dt=end_date, - ext=external)) + return await self.api_request( + urls.historic_trades.format( + m=market.value, s=ticker, df=start_date, dt=end_date, ext=external + ) + ) - - def get_segments(self): + async def get_segments(self): """Make a request to the API and get a list of valid segments. For more detailed information go to: https://apihub.primary.com.ar/assets/docs/Primary-API.pdf @@ -78,9 +77,9 @@ def get_segments(self): :return: A list of valid ROFEXs segments returned by the API. :rtype: dict of JSON response. """ - return self.api_request(urls.segments) + return await self.api_request(urls.segments) - def get_instruments(self, endpoint, **kwargs): + async def get_instruments(self, endpoint, **kwargs): """Make a request to the API and get the information depending on the given endpoint. Valid 'endpoints' are: 'all', 'details', 'detail', 'by_cfi', 'by_segments'. @@ -94,13 +93,15 @@ def get_instruments(self, endpoint, **kwargs): """ # Check if endpoint arg is a valid one. if endpoint not in urls.instruments.keys(): - raise ApiException("Valid endpoints are: 'all', 'details', 'detail', 'by_cfi', 'by_segments'") + raise ApiException( + "Valid endpoints are: 'all', 'details', 'detail', 'by_cfi', 'by_segments'" + ) # Get url template url = urls.instruments[endpoint] # Define the regular expression pattern to get value between {} in a string - pattern = r'\{(.*?)\}' + pattern = r"\{(.*?)\}" # Find all required args in url template required_args = re.findall(pattern, url) @@ -127,17 +128,26 @@ def get_instruments(self, endpoint, **kwargs): for i in v: kwargs[k] = i if not response: - response = self.api_request(urls.instruments[endpoint].format(**kwargs)) + response = await self.api_request( + urls.instruments[endpoint].format(**kwargs) + ) else: - response['instruments'] = \ - response['instruments'] + \ - self.api_request(urls.instruments[endpoint].format(**kwargs))['instruments'] + response["instruments"] = ( + response["instruments"] + + ( + await self.api_request( + urls.instruments[endpoint].format(**kwargs) + ) + )["instruments"] + ) if not response: - response = self.api_request(urls.instruments[endpoint].format(**kwargs)) + response = await self.api_request( + urls.instruments[endpoint].format(**kwargs) + ) return response - def get_all_instruments(self): + async def get_all_instruments(self): """Make a request to the API and get a list of all available instruments. For more detailed information go to: https://apihub.primary.com.ar/assets/docs/Primary-API.pdf @@ -145,9 +155,9 @@ def get_all_instruments(self): :return: A list of valid instruments returned by the API. :rtype: dict of JSON response. """ - return self.api_request(urls.instruments['all']) + return await self.api_request(urls.instruments["all"]) - def get_detailed_instruments(self): + async def get_detailed_instruments(self): """Make a request to the API and get a list of all available instruments. For more detailed information go to: https://apihub.primary.com.ar/assets/docs/Primary-API.pdf @@ -155,9 +165,9 @@ def get_detailed_instruments(self): :return: A list of valid instruments returned by the API. :rtype: dict of JSON response. """ - return self.api_request(urls.instruments['details']) + return await self.api_request(urls.instruments["details"]) - def get_instrument_details(self, ticker, market): + async def get_instrument_details(self, ticker, market): """Make a request to the API and get the details of the instrument. For more detailed information go to: https://apihub.primary.com.ar/assets/docs/Primary-API.pdf @@ -169,9 +179,11 @@ def get_instrument_details(self, ticker, market): :return: Details of the instrument returned by the API. :rtype: dict of JSON response. """ - return self.api_request(urls.instruments['detail'].format(ticker=ticker, market=market.value)) + return await self.api_request( + urls.instruments["detail"].format(ticker=ticker, market=market.value) + ) - def get_market_data(self, ticker, entries, depth, market): + async def get_market_data(self, ticker, entries, depth, market): """Make a request to the API to get the Market Data Entries of the specified instrument. For more detailed information go to: https://apihub.primary.com.ar/assets/docs/Primary-API.pdf @@ -190,12 +202,11 @@ def get_market_data(self, ticker, entries, depth, market): # Creates a comma separated string with the entries in the list. entry_string = ",".join([entry.value for entry in entries]) - return self.api_request(urls.market_data.format(m=market.value, - s=ticker, - e=entry_string, - d=depth)) + return await self.api_request( + urls.market_data.format(m=market.value, s=ticker, e=entry_string, d=depth) + ) - def get_order_status(self, client_order_id, proprietary): + async def get_order_status(self, client_order_id, proprietary): """Make a request to the API to get the status of the specified order. For more detailed information go to: https://apihub.primary.com.ar/assets/docs/Primary-API.pdf @@ -207,10 +218,11 @@ def get_order_status(self, client_order_id, proprietary): :return: Order status response of the API. :rtype: dict of JSON response. """ - return self.api_request(urls.order_status.format(c=client_order_id, - p=proprietary)) + return await self.api_request( + urls.order_status.format(c=client_order_id, p=proprietary) + ) - def get_all_orders_by_account(self, account): + async def get_all_orders_by_account(self, account): """Make a request to the API and get the status of all the orders associated with the account. For more detailed information go to: https://apihub.primary.com.ar/assets/docs/Primary-API.pdf @@ -220,9 +232,9 @@ def get_all_orders_by_account(self, account): :return: List of all orders status associated with the user returned by the API. :rtype: dict of JSON response. """ - return self.api_request(urls.all_orders_status.format(a=account)) + return await self.api_request(urls.all_orders_status.format(a=account)) - def get_account_position(self, account): + async def get_account_position(self, account): """Make a request to the API and get the account positions. For more detailed information go to: https://apihub.primary.com.ar/assets/docs/Primary-API.pdf @@ -232,9 +244,9 @@ def get_account_position(self, account): :return: List of all instruments positions status associated with the user returned by the API. :rtype: dict of JSON response. """ - return self.api_request(urls.account_position.format(a=account)) + return await self.api_request(urls.account_position.format(a=account)) - def get_detailed_position(self, account): + async def get_detailed_position(self, account): """Make a request to the API and get the detailed account asset positions by asset type. For more detailed information go to: https://apihub.primary.com.ar/assets/docs/Primary-API.pdf @@ -244,9 +256,9 @@ def get_detailed_position(self, account): :return: List of all instruments positions status associated with the user returned by the API. :rtype: dict of JSON response. """ - return self.api_request(urls.detailed_position.format(a=account)) + return await self.api_request(urls.detailed_position.format(a=account)) - def get_account_report(self, account): + async def get_account_report(self, account): """Make a request to the API and get the summary of associated account. For more detailed information go to: https://apihub.primary.com.ar/assets/docs/Primary-API.pdf @@ -256,12 +268,23 @@ def get_account_report(self, account): :return: Summary status associated with the user returned by the API. :rtype: dict of JSON response. """ - return self.api_request(urls.account_report.format(a=account)) - - def send_order(self, ticker, size, order_type, side, - account, price, time_in_force, market, - cancel_previous, iceberg, expire_date, - display_quantity): + return await self.api_request(urls.account_report.format(a=account)) + + async def send_order( + self, + ticker, + size, + order_type, + side, + account, + price, + time_in_force, + market, + cancel_previous, + iceberg, + expire_date, + display_quantity, + ): """Make a request to the API that send a new order to the Market. For more detailed information go to: https://apihub.primary.com.ar/assets/docs/Primary-API.pdf @@ -306,20 +329,24 @@ def send_order(self, ticker, size, order_type, side, if iceberg: new_order_url = new_order_url + urls.iceberg - return self.api_request(new_order_url.format(market=market.value, - ticker=ticker, - size=size, - type=order_type.value, - side=side.value, - time_force=time_in_force.value, - account=account, - price=price, - cancel_previous=cancel_previous, - iceberg=iceberg, - expire_date=expire_date, - display_quantity=display_quantity)) - - def cancel_order(self, client_order_id, proprietary): + return await self.api_request( + new_order_url.format( + market=market.value, + ticker=ticker, + size=size, + type=order_type.value, + side=side.value, + time_force=time_in_force.value, + account=account, + price=price, + cancel_previous=cancel_previous, + iceberg=iceberg, + expire_date=expire_date, + display_quantity=display_quantity, + ) + ) + + async def cancel_order(self, client_order_id, proprietary): """Make a request to the API and cancel the order specified. The market will respond with a client order id, then you should verify the status of the request with this id. @@ -333,11 +360,12 @@ def cancel_order(self, client_order_id, proprietary): :return: Client Order ID of cancellation request returned by the API. :rtype: dict of JSON response. """ - return self.api_request(urls.cancel_order.format(id=client_order_id, - p=proprietary)) + return await self.api_request( + urls.cancel_order.format(id=client_order_id, p=proprietary) + ) - def api_request(self, path, retry=True): - """ Make a GET request to the API. + async def api_request(self, path, retry=True): + """Make a GET request to the API. :param path: path to the API resource. :type path: str @@ -347,42 +375,46 @@ def api_request(self, path, retry=True): :return: response of the API. :rtype: dict of JSON response. """ - headers = {'X-Auth-Token': self.environment["token"]} - response = requests.get(self._url(path), - headers=headers, - verify=self.environment["ssl"], - proxies=self.environment["proxies"]) + headers = {"X-Auth-Token": self.environment["token"]} + proxies = self.environment.get("proxies") + async with httpx.AsyncClient( + verify=self.environment["ssl"], proxy=proxies + ) as client: + response = await client.get(self._url(path), headers=headers) # Checks if the response code is 401 (Unauthorized) if response.status_code == 401: if retry: - self.update_token() - self.api_request(path, False) + await self.update_token() + return await self.api_request(path, False) else: raise ApiException("Authentication Fails.") return simplejson.loads(response.content) - def update_token(self): - """ Authenticate using the environment user and password. + async def update_token(self): + """Authenticate using the environment user and password. Then save the token in the environment parameters and set the initialized parameter to True. """ - headers = {'X-Username': self.environment["user"], - 'X-Password': self.environment["password"]} - response = requests.post(self._url(urls.auth), - headers=headers, - verify=self.environment["ssl"], - proxies=self.environment["proxies"]) - - if not response.ok: + headers = { + "X-Username": self.environment["user"], + "X-Password": self.environment["password"], + } + proxies = self.environment.get("proxies") + async with httpx.AsyncClient( + verify=self.environment["ssl"], proxy=proxies + ) as client: + response = await client.post(self._url(urls.auth), headers=headers) + + if not response.is_success: raise ApiException("Authentication fails. Incorrect User or Password") - self.environment["token"] = response.headers['X-Auth-Token'] + self.environment["token"] = response.headers["X-Auth-Token"] self.environment["initialized"] = True def _url(self, path): - """ Helper function that concatenate the path to the environment url. + """Helper function that concatenate the path to the environment url. :param path: path to the API resource. :type path: str diff --git a/src/pyRofex/service.py b/src/pyRofex/service.py index f2be83d..ccd0561 100644 --- a/src/pyRofex/service.py +++ b/src/pyRofex/service.py @@ -5,15 +5,15 @@ All the library exposed functionality """ +import asyncio import logging from inspect import getfullargspec from .clients.rest_rfx import RestClient from .clients.websocket_rfx import WebSocketClient from .components import globals -from .components.enums import Environment, Market, MarketDataEntry, TimeInForce +from .components.enums import Broker, Environment, Market, MarketDataEntry, TimeInForce from .components.exceptions import ApiException -from .components.enums import Broker # ###################################################### # ## Initialization functions ## @@ -28,7 +28,7 @@ def initialize( proxies=None, ssl_opt=None, active_token=None, - broker : Broker = None, + broker: Broker = None, ): """Initialize the specified environment. @@ -149,8 +149,8 @@ def get_segments(environment=None): _validate_initialization(environment) # Get the client for the environment and start the connection - response = globals.environment_config[environment]["rest_client"].get_segments() - return response + client: RestClient = globals.environment_config[environment]["rest_client"] + return asyncio.run(client.get_segments()) def get_instruments(endpoint="all", environment=None, **kwargs): @@ -173,8 +173,8 @@ def get_instruments(endpoint="all", environment=None, **kwargs): _validate_initialization(environment) # Get the client for the environment and start the connection - client = globals.environment_config[environment]["rest_client"] - return client.get_instruments(endpoint, **kwargs) + client: RestClient = globals.environment_config[environment]["rest_client"] + return asyncio.run(client.get_instruments(endpoint, **kwargs)) def get_all_instruments(environment=None): @@ -193,8 +193,8 @@ def get_all_instruments(environment=None): _validate_initialization(environment) # Get the client for the environment and make the request - client = globals.environment_config[environment]["rest_client"] - return client.get_all_instruments() + client: RestClient = globals.environment_config[environment]["rest_client"] + return asyncio.run(client.get_all_instruments()) def get_detailed_instruments(environment=None): @@ -213,8 +213,8 @@ def get_detailed_instruments(environment=None): _validate_initialization(environment) # Get the client for the environment and make the request - client = globals.environment_config[environment]["rest_client"] - return client.get_detailed_instruments() + client: RestClient = globals.environment_config[environment]["rest_client"] + return asyncio.run(client.get_detailed_instruments()) def get_instrument_details(ticker, market=Market.ROFEX, environment=None): @@ -237,8 +237,8 @@ def get_instrument_details(ticker, market=Market.ROFEX, environment=None): _validate_initialization(environment) # Get the client for the environment and make the request - client = globals.environment_config[environment]["rest_client"] - return client.get_instrument_details(ticker, market) + client: RestClient = globals.environment_config[environment]["rest_client"] + return asyncio.run(client.get_instrument_details(ticker, market)) def get_market_data( @@ -269,8 +269,8 @@ def get_market_data( entries = _validate_market_data_entries(entries) # Get the client for the environment and make the request - client = globals.environment_config[environment]["rest_client"] - return client.get_market_data(ticker, entries, depth, market) + client: RestClient = globals.environment_config[environment]["rest_client"] + return asyncio.run(client.get_market_data(ticker, entries, depth, market)) def get_order_status(client_order_id, proprietary=None, environment=None): @@ -297,8 +297,8 @@ def get_order_status(client_order_id, proprietary=None, environment=None): proprietary = globals.environment_config[environment]["proprietary"] # Get the client for the environment and make the request - client = globals.environment_config[environment]["rest_client"] - return client.get_order_status(client_order_id, proprietary) + client: RestClient = globals.environment_config[environment]["rest_client"] + return asyncio.run(client.get_order_status(client_order_id, proprietary)) def send_order( @@ -361,20 +361,22 @@ def send_order( account = globals.environment_config[environment]["account"] # Get the client for the environment and make the request - client = globals.environment_config[environment]["rest_client"] - return client.send_order( - ticker, - size, - order_type, - side, - account, - price, - time_in_force, - market, - cancel_previous, - iceberg, - expire_date, - display_quantity, + client: RestClient = globals.environment_config[environment]["rest_client"] + return asyncio.run( + client.send_order( + ticker, + size, + order_type, + side, + account, + price, + time_in_force, + market, + cancel_previous, + iceberg, + expire_date, + display_quantity, + ) ) @@ -404,8 +406,8 @@ def cancel_order(client_order_id, proprietary=None, environment=None): proprietary = globals.environment_config[environment]["proprietary"] # Get the client for the environment and make the request - client = globals.environment_config[environment]["rest_client"] - return client.cancel_order(client_order_id, proprietary) + client: RestClient = globals.environment_config[environment]["rest_client"] + return asyncio.run(client.cancel_order(client_order_id, proprietary)) def get_all_orders_status(account=None, environment=None): @@ -431,8 +433,8 @@ def get_all_orders_status(account=None, environment=None): account = globals.environment_config[environment]["account"] # Get the client for the environment and make the request - client = globals.environment_config[environment]["rest_client"] - return client.get_all_orders_by_account(account) + client: RestClient = globals.environment_config[environment]["rest_client"] + return asyncio.run(client.get_all_orders_by_account(account)) def get_trade_history( @@ -461,8 +463,8 @@ def get_trade_history( _validate_initialization(environment) # Get the client for the environment and make the request - client = globals.environment_config[environment]["rest_client"] - return client.get_trade_history(ticker, start_date, end_date, market) + client: RestClient = globals.environment_config[environment]["rest_client"] + return asyncio.run(client.get_trade_history(ticker, start_date, end_date, market)) def get_account_position(account=None, environment=None): @@ -488,8 +490,8 @@ def get_account_position(account=None, environment=None): account = globals.environment_config[environment]["account"] # Get the client for the environment and make the request - client = globals.environment_config[environment]["rest_client"] - return client.get_account_position(account) + client: RestClient = globals.environment_config[environment]["rest_client"] + return asyncio.run(client.get_account_position(account)) def get_detailed_position(account=None, environment=None): @@ -515,8 +517,8 @@ def get_detailed_position(account=None, environment=None): account = globals.environment_config[environment]["account"] # Get the client for the environment and make the request - client = globals.environment_config[environment]["rest_client"] - return client.get_detailed_position(account) + client: RestClient = globals.environment_config[environment]["rest_client"] + return asyncio.run(client.get_detailed_position(account)) def get_account_report(account=None, environment=None): @@ -542,8 +544,8 @@ def get_account_report(account=None, environment=None): account = globals.environment_config[environment]["account"] # Get the client for the environment and make the request - client = globals.environment_config[environment]["rest_client"] - return client.get_account_report(account) + client: RestClient = globals.environment_config[environment]["rest_client"] + return asyncio.run(client.get_account_report(account)) # ###################################################### @@ -579,7 +581,7 @@ def init_websocket_connection( _validate_initialization(environment) # Gets the client for the environment - client = globals.environment_config[environment]["ws_client"] + client: WebSocketClient = globals.environment_config[environment]["ws_client"] # Checks handlers and adds them into the client. if market_data_handler is not None: @@ -613,7 +615,7 @@ def close_websocket_connection(environment=None): environment = _validate_environment(environment) # Gets the client for the environment - client = globals.environment_config[environment]["ws_client"] + client: WebSocketClient = globals.environment_config[environment]["ws_client"] # Close Websocket connection with the API client.close_connection() @@ -653,7 +655,7 @@ def order_report_subscription( ) # Get the client for the environment and send the subscription message - client = globals.environment_config[environment]["ws_client"] + client: WebSocketClient = globals.environment_config[environment]["ws_client"] client.order_report_subscription(account, snapshot) @@ -691,7 +693,7 @@ def market_data_subscription( ) # Get the client for the environment and send the subscription message - client = globals.environment_config[environment]["ws_client"] + client: WebSocketClient = globals.environment_config[environment]["ws_client"] client.market_data_subscription(tickers, entries, market, depth) @@ -712,7 +714,7 @@ def add_websocket_market_data_handler(handler, environment=None): _validate_handler(handler) # Get the client for the environment and adds the handler - client = globals.environment_config[environment]["ws_client"] + client: WebSocketClient = globals.environment_config[environment]["ws_client"] client.add_market_data_handler(handler) @@ -733,7 +735,7 @@ def add_websocket_order_report_handler(handler, environment=None): _validate_handler(handler) # Get the client for the environment and adds the handler - client = globals.environment_config[environment]["ws_client"] + client: WebSocketClient = globals.environment_config[environment]["ws_client"] client.add_order_report_handler(handler) @@ -754,7 +756,7 @@ def add_websocket_error_handler(handler, environment=None): _validate_handler(handler) # Get the client for the environment and adds the handler - client = globals.environment_config[environment]["ws_client"] + client: WebSocketClient = globals.environment_config[environment]["ws_client"] client.add_error_handler(handler) @@ -772,7 +774,7 @@ def remove_websocket_market_data_handler(handler, environment=None): _validate_initialization(environment) # Get the client for the environment and adds the handler - client = globals.environment_config[environment]["ws_client"] + client: WebSocketClient = globals.environment_config[environment]["ws_client"] client.remove_market_data_handler(handler) @@ -790,7 +792,7 @@ def remove_websocket_order_report_handler(handler, environment=None): _validate_initialization(environment) # Get the client for the environment and adds the handler - client = globals.environment_config[environment]["ws_client"] + client: WebSocketClient = globals.environment_config[environment]["ws_client"] client.remove_order_report_handler(handler) @@ -808,7 +810,7 @@ def remove_websocket_error_handler(handler, environment=None): _validate_initialization(environment) # Get the client for the environment and adds the handler - client = globals.environment_config[environment]["ws_client"] + client: WebSocketClient = globals.environment_config[environment]["ws_client"] client.remove_error_handler(handler) @@ -829,7 +831,7 @@ def set_websocket_exception_handler(handler, environment=None): _validate_handler(handler) # Get the client for the environment and adds the handler - client = globals.environment_config[environment]["ws_client"] + client: WebSocketClient = globals.environment_config[environment]["ws_client"] client.set_exception_handler(handler) @@ -857,7 +859,7 @@ def cancel_order_via_websocket(client_order_id, proprietary=None, environment=No proprietary = globals.environment_config[environment]["proprietary"] # Get the client for the environment and make the request - client = globals.environment_config[environment]["ws_client"] + client: WebSocketClient = globals.environment_config[environment]["ws_client"] client.cancel_order(client_order_id, proprietary) @@ -920,7 +922,7 @@ def send_order_via_websocket( _validate_initialization(environment) # Gets the client for the environment - client = globals.environment_config[environment]["ws_client"] + client: WebSocketClient = globals.environment_config[environment]["ws_client"] # Checks the account and sets the default one if None is received. if account is None: diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..ad1325e --- /dev/null +++ b/uv.lock @@ -0,0 +1,193 @@ +version = 1 +revision = 1 +requires-python = ">=3.9" + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592 }, +] + +[[package]] +name = "certifi" +version = "2026.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900 }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740 }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515 }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784 }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008 }, +] + +[[package]] +name = "pyrofex" +version = "0.5.1" +source = { editable = "." } +dependencies = [ + { name = "httpx" }, + { name = "simplejson" }, + { name = "websocket-client" }, +] + +[package.metadata] +requires-dist = [ + { name = "httpx", specifier = ">=0.28.1" }, + { name = "simplejson", specifier = ">=3.20.0" }, + { name = "websocket-client", specifier = ">=1.9.0" }, +] + +[[package]] +name = "simplejson" +version = "3.20.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/f4/a1ac5ed32f7ed9a088d62a59d410d4c204b3b3815722e2ccfb491fa8251b/simplejson-3.20.2.tar.gz", hash = "sha256:5fe7a6ce14d1c300d80d08695b7f7e633de6cd72c80644021874d985b3393649", size = 85784 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/09/2bf3761de89ea2d91bdce6cf107dcd858892d0adc22c995684878826cc6b/simplejson-3.20.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6d7286dc11af60a2f76eafb0c2acde2d997e87890e37e24590bb513bec9f1bc5", size = 94039 }, + { url = "https://files.pythonhosted.org/packages/0f/33/c3277db8931f0ae9e54b9292668863365672d90fb0f632f4cf9829cb7d68/simplejson-3.20.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c01379b4861c3b0aa40cba8d44f2b448f5743999aa68aaa5d3ef7049d4a28a2d", size = 75894 }, + { url = "https://files.pythonhosted.org/packages/fa/ea/ae47b04d03c7c8a7b7b1a8b39a6e27c3bd424e52f4988d70aca6293ff5e5/simplejson-3.20.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a16b029ca25645b3bc44e84a4f941efa51bf93c180b31bd704ce6349d1fc77c1", size = 76116 }, + { url = "https://files.pythonhosted.org/packages/4b/42/6c9af551e5a8d0f171d6dce3d9d1260068927f7b80f1f09834e07887c8c4/simplejson-3.20.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e22a5fb7b1437ffb057e02e1936a3bfb19084ae9d221ec5e9f4cf85f69946b6", size = 138827 }, + { url = "https://files.pythonhosted.org/packages/2b/22/5e268bbcbe9f75577491e406ec0a5536f5b2fa91a3b52031fea51cd83e1d/simplejson-3.20.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8b6ff02fc7b8555c906c24735908854819b0d0dc85883d453e23ca4c0445d01", size = 146772 }, + { url = "https://files.pythonhosted.org/packages/71/b4/800f14728e2ad666f420dfdb57697ca128aeae7f991b35759c09356b829a/simplejson-3.20.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2bfc1c396ad972ba4431130b42307b2321dba14d988580c1ac421ec6a6b7cee3", size = 134497 }, + { url = "https://files.pythonhosted.org/packages/c1/b9/c54eef4226c6ac8e9a389bbe5b21fef116768f97a2dc1a683c716ffe66ef/simplejson-3.20.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a97249ee1aee005d891b5a211faf58092a309f3d9d440bc269043b08f662eda", size = 138172 }, + { url = "https://files.pythonhosted.org/packages/09/36/4e282f5211b34620f1b2e4b51d9ddaab5af82219b9b7b78360a33f7e5387/simplejson-3.20.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f1036be00b5edaddbddbb89c0f80ed229714a941cfd21e51386dc69c237201c2", size = 140272 }, + { url = "https://files.pythonhosted.org/packages/aa/b0/94ad2cf32f477c449e1f63c863d8a513e2408d651c4e58fe4b6a7434e168/simplejson-3.20.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5d6f5bacb8cdee64946b45f2680afa3f54cd38e62471ceda89f777693aeca4e4", size = 140468 }, + { url = "https://files.pythonhosted.org/packages/e5/46/827731e4163be3f987cb8ee90f5d444161db8f540b5e735355faa098d9bc/simplejson-3.20.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8db6841fb796ec5af632f677abf21c6425a1ebea0d9ac3ef1a340b8dc69f52b8", size = 148700 }, + { url = "https://files.pythonhosted.org/packages/c7/28/c32121064b1ec2fb7b5d872d9a1abda62df064d35e0160eddfa907118343/simplejson-3.20.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c0a341f7cc2aae82ee2b31f8a827fd2e51d09626f8b3accc441a6907c88aedb7", size = 141323 }, + { url = "https://files.pythonhosted.org/packages/46/b6/c897c54326fe86dd12d101981171a49361949f4728294f418c3b86a1af77/simplejson-3.20.2-cp310-cp310-win32.whl", hash = "sha256:27f9c01a6bc581d32ab026f515226864576da05ef322d7fc141cd8a15a95ce53", size = 74377 }, + { url = "https://files.pythonhosted.org/packages/ad/87/a6e03d4d80cca99c1fee4e960f3440e2f21be9470e537970f960ca5547f1/simplejson-3.20.2-cp310-cp310-win_amd64.whl", hash = "sha256:c0a63ec98a4547ff366871bf832a7367ee43d047bcec0b07b66c794e2137b476", size = 76081 }, + { url = "https://files.pythonhosted.org/packages/b9/3e/96898c6c66d9dca3f9bd14d7487bf783b4acc77471b42f979babbb68d4ca/simplejson-3.20.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:06190b33cd7849efc413a5738d3da00b90e4a5382fd3d584c841ac20fb828c6f", size = 92633 }, + { url = "https://files.pythonhosted.org/packages/6b/a2/cd2e10b880368305d89dd540685b8bdcc136df2b3c76b5ddd72596254539/simplejson-3.20.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4ad4eac7d858947a30d2c404e61f16b84d16be79eb6fb316341885bdde864fa8", size = 75309 }, + { url = "https://files.pythonhosted.org/packages/5d/02/290f7282eaa6ebe945d35c47e6534348af97472446951dce0d144e013f4c/simplejson-3.20.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b392e11c6165d4a0fde41754a0e13e1d88a5ad782b245a973dd4b2bdb4e5076a", size = 75308 }, + { url = "https://files.pythonhosted.org/packages/43/91/43695f17b69e70c4b0b03247aa47fb3989d338a70c4b726bbdc2da184160/simplejson-3.20.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51eccc4e353eed3c50e0ea2326173acdc05e58f0c110405920b989d481287e51", size = 143733 }, + { url = "https://files.pythonhosted.org/packages/9b/4b/fdcaf444ac1c3cbf1c52bf00320c499e1cf05d373a58a3731ae627ba5e2d/simplejson-3.20.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:306e83d7c331ad833d2d43c76a67f476c4b80c4a13334f6e34bb110e6105b3bd", size = 153397 }, + { url = "https://files.pythonhosted.org/packages/c4/83/21550f81a50cd03599f048a2d588ffb7f4c4d8064ae091511e8e5848eeaa/simplejson-3.20.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f820a6ac2ef0bc338ae4963f4f82ccebdb0824fe9caf6d660670c578abe01013", size = 141654 }, + { url = "https://files.pythonhosted.org/packages/cf/54/d76c0e72ad02450a3e723b65b04f49001d0e73218ef6a220b158a64639cb/simplejson-3.20.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21e7a066528a5451433eb3418184f05682ea0493d14e9aae690499b7e1eb6b81", size = 144913 }, + { url = "https://files.pythonhosted.org/packages/3f/49/976f59b42a6956d4aeb075ada16ad64448a985704bc69cd427a2245ce835/simplejson-3.20.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:438680ddde57ea87161a4824e8de04387b328ad51cfdf1eaf723623a3014b7aa", size = 144568 }, + { url = "https://files.pythonhosted.org/packages/60/c7/30bae30424ace8cd791ca660fed454ed9479233810fe25c3f3eab3d9dc7b/simplejson-3.20.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:cac78470ae68b8d8c41b6fca97f5bf8e024ca80d5878c7724e024540f5cdaadb", size = 146239 }, + { url = "https://files.pythonhosted.org/packages/79/3e/7f3b7b97351c53746e7b996fcd106986cda1954ab556fd665314756618d2/simplejson-3.20.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7524e19c2da5ef281860a3d74668050c6986be15c9dd99966034ba47c68828c2", size = 154497 }, + { url = "https://files.pythonhosted.org/packages/1d/48/7241daa91d0bf19126589f6a8dcbe8287f4ed3d734e76fd4a092708947be/simplejson-3.20.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e9b6d845a603b2eef3394eb5e21edb8626cd9ae9a8361d14e267eb969dbe413", size = 148069 }, + { url = "https://files.pythonhosted.org/packages/e6/f4/ef18d2962fe53e7be5123d3784e623859eec7ed97060c9c8536c69d34836/simplejson-3.20.2-cp311-cp311-win32.whl", hash = "sha256:47d8927e5ac927fdd34c99cc617938abb3624b06ff86e8e219740a86507eb961", size = 74158 }, + { url = "https://files.pythonhosted.org/packages/35/fd/3d1158ecdc573fdad81bf3cc78df04522bf3959758bba6597ba4c956c74d/simplejson-3.20.2-cp311-cp311-win_amd64.whl", hash = "sha256:ba4edf3be8e97e4713d06c3d302cba1ff5c49d16e9d24c209884ac1b8455520c", size = 75911 }, + { url = "https://files.pythonhosted.org/packages/9d/9e/1a91e7614db0416885eab4136d49b7303de20528860ffdd798ce04d054db/simplejson-3.20.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:4376d5acae0d1e91e78baeba4ee3cf22fbf6509d81539d01b94e0951d28ec2b6", size = 93523 }, + { url = "https://files.pythonhosted.org/packages/5e/2b/d2413f5218fc25608739e3d63fe321dfa85c5f097aa6648dbe72513a5f12/simplejson-3.20.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f8fe6de652fcddae6dec8f281cc1e77e4e8f3575249e1800090aab48f73b4259", size = 75844 }, + { url = "https://files.pythonhosted.org/packages/ad/f1/efd09efcc1e26629e120fef59be059ce7841cc6e1f949a4db94f1ae8a918/simplejson-3.20.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25ca2663d99328d51e5a138f22018e54c9162438d831e26cfc3458688616eca8", size = 75655 }, + { url = "https://files.pythonhosted.org/packages/97/ec/5c6db08e42f380f005d03944be1af1a6bd501cc641175429a1cbe7fb23b9/simplejson-3.20.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12a6b2816b6cab6c3fd273d43b1948bc9acf708272074c8858f579c394f4cbc9", size = 150335 }, + { url = "https://files.pythonhosted.org/packages/81/f5/808a907485876a9242ec67054da7cbebefe0ee1522ef1c0be3bfc90f96f6/simplejson-3.20.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac20dc3fcdfc7b8415bfc3d7d51beccd8695c3f4acb7f74e3a3b538e76672868", size = 158519 }, + { url = "https://files.pythonhosted.org/packages/66/af/b8a158246834645ea890c36136584b0cc1c0e4b83a73b11ebd9c2a12877c/simplejson-3.20.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db0804d04564e70862ef807f3e1ace2cc212ef0e22deb1b3d6f80c45e5882c6b", size = 148571 }, + { url = "https://files.pythonhosted.org/packages/20/05/ed9b2571bbf38f1a2425391f18e3ac11cb1e91482c22d644a1640dea9da7/simplejson-3.20.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:979ce23ea663895ae39106946ef3d78527822d918a136dbc77b9e2b7f006237e", size = 152367 }, + { url = "https://files.pythonhosted.org/packages/81/2c/bad68b05dd43e93f77994b920505634d31ed239418eb6a88997d06599983/simplejson-3.20.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a2ba921b047bb029805726800819675249ef25d2f65fd0edb90639c5b1c3033c", size = 150205 }, + { url = "https://files.pythonhosted.org/packages/69/46/90c7fc878061adafcf298ce60cecdee17a027486e9dce507e87396d68255/simplejson-3.20.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:12d3d4dc33770069b780cc8f5abef909fe4a3f071f18f55f6d896a370fd0f970", size = 151823 }, + { url = "https://files.pythonhosted.org/packages/ab/27/b85b03349f825ae0f5d4f780cdde0bbccd4f06c3d8433f6a3882df887481/simplejson-3.20.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:aff032a59a201b3683a34be1169e71ddda683d9c3b43b261599c12055349251e", size = 158997 }, + { url = "https://files.pythonhosted.org/packages/71/ad/d7f3c331fb930638420ac6d236db68e9f4c28dab9c03164c3cd0e7967e15/simplejson-3.20.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:30e590e133b06773f0dc9c3f82e567463df40598b660b5adf53eb1c488202544", size = 154367 }, + { url = "https://files.pythonhosted.org/packages/f0/46/5c67324addd40fa2966f6e886cacbbe0407c03a500db94fb8bb40333fcdf/simplejson-3.20.2-cp312-cp312-win32.whl", hash = "sha256:8d7be7c99939cc58e7c5bcf6bb52a842a58e6c65e1e9cdd2a94b697b24cddb54", size = 74285 }, + { url = "https://files.pythonhosted.org/packages/fa/c9/5cc2189f4acd3a6e30ffa9775bf09b354302dbebab713ca914d7134d0f29/simplejson-3.20.2-cp312-cp312-win_amd64.whl", hash = "sha256:2c0b4a67e75b945489052af6590e7dca0ed473ead5d0f3aad61fa584afe814ab", size = 75969 }, + { url = "https://files.pythonhosted.org/packages/5e/9e/f326d43f6bf47f4e7704a4426c36e044c6bedfd24e072fb8e27589a373a5/simplejson-3.20.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:90d311ba8fcd733a3677e0be21804827226a57144130ba01c3c6a325e887dd86", size = 93530 }, + { url = "https://files.pythonhosted.org/packages/35/28/5a4b8f3483fbfb68f3f460bc002cef3a5735ef30950e7c4adce9c8da15c7/simplejson-3.20.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:feed6806f614bdf7f5cb6d0123cb0c1c5f40407ef103aa935cffaa694e2e0c74", size = 75846 }, + { url = "https://files.pythonhosted.org/packages/7a/4d/30dfef83b9ac48afae1cf1ab19c2867e27b8d22b5d9f8ca7ce5a0a157d8c/simplejson-3.20.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6b1d8d7c3e1a205c49e1aee6ba907dcb8ccea83651e6c3e2cb2062f1e52b0726", size = 75661 }, + { url = "https://files.pythonhosted.org/packages/09/1d/171009bd35c7099d72ef6afd4bb13527bab469965c968a17d69a203d62a6/simplejson-3.20.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:552f55745044a24c3cb7ec67e54234be56d5d6d0e054f2e4cf4fb3e297429be5", size = 150579 }, + { url = "https://files.pythonhosted.org/packages/61/ae/229bbcf90a702adc6bfa476e9f0a37e21d8c58e1059043038797cbe75b8c/simplejson-3.20.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2da97ac65165d66b0570c9e545786f0ac7b5de5854d3711a16cacbcaa8c472d", size = 158797 }, + { url = "https://files.pythonhosted.org/packages/90/c5/fefc0ac6b86b9108e302e0af1cf57518f46da0baedd60a12170791d56959/simplejson-3.20.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f59a12966daa356bf68927fca5a67bebac0033cd18b96de9c2d426cd11756cd0", size = 148851 }, + { url = "https://files.pythonhosted.org/packages/43/f1/b392952200f3393bb06fbc4dd975fc63a6843261705839355560b7264eb2/simplejson-3.20.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133ae2098a8e162c71da97cdab1f383afdd91373b7ff5fe65169b04167da976b", size = 152598 }, + { url = "https://files.pythonhosted.org/packages/f4/b4/d6b7279e52a3e9c0fa8c032ce6164e593e8d9cf390698ee981ed0864291b/simplejson-3.20.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7977640af7b7d5e6a852d26622057d428706a550f7f5083e7c4dd010a84d941f", size = 150498 }, + { url = "https://files.pythonhosted.org/packages/62/22/ec2490dd859224326d10c2fac1353e8ad5c84121be4837a6dd6638ba4345/simplejson-3.20.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b530ad6d55e71fa9e93e1109cf8182f427a6355848a4ffa09f69cc44e1512522", size = 152129 }, + { url = "https://files.pythonhosted.org/packages/33/ce/b60214d013e93dd9e5a705dcb2b88b6c72bada442a97f79828332217f3eb/simplejson-3.20.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bd96a7d981bf64f0e42345584768da4435c05b24fd3c364663f5fbc8fabf82e3", size = 159359 }, + { url = "https://files.pythonhosted.org/packages/99/21/603709455827cdf5b9d83abe726343f542491ca8dc6a2528eb08de0cf034/simplejson-3.20.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f28ee755fadb426ba2e464d6fcf25d3f152a05eb6b38e0b4f790352f5540c769", size = 154717 }, + { url = "https://files.pythonhosted.org/packages/3c/f9/dc7f7a4bac16cf7eb55a4df03ad93190e11826d2a8950052949d3dfc11e2/simplejson-3.20.2-cp313-cp313-win32.whl", hash = "sha256:472785b52e48e3eed9b78b95e26a256f59bb1ee38339be3075dad799e2e1e661", size = 74289 }, + { url = "https://files.pythonhosted.org/packages/87/10/d42ad61230436735c68af1120622b28a782877146a83d714da7b6a2a1c4e/simplejson-3.20.2-cp313-cp313-win_amd64.whl", hash = "sha256:a1a85013eb33e4820286139540accbe2c98d2da894b2dcefd280209db508e608", size = 75972 }, + { url = "https://files.pythonhosted.org/packages/b8/2d/7c4968c60ddc8b504b77301cc80d6e75cd0269b81a779b01d66d8f36dcb8/simplejson-3.20.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b3bf76512ccb07d47944ebdca44c65b781612d38b9098566b4bb40f713fc4047", size = 94039 }, + { url = "https://files.pythonhosted.org/packages/e8/e4/d96b56fb87f245240b514c1fe552e76c17e09f0faa1f61137b2296f81529/simplejson-3.20.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:214e26acf2dfb9ff3314e65c4e168a6b125bced0e2d99a65ea7b0f169db1e562", size = 75893 }, + { url = "https://files.pythonhosted.org/packages/09/4f/be411eeb52ab21d6d4c00722b632dd2bd430c01a47dfed3c15ef5ad7ee6e/simplejson-3.20.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2fb1259ca9c385b0395bad59cdbf79535a5a84fb1988f339a49bfbc57455a35a", size = 76104 }, + { url = "https://files.pythonhosted.org/packages/66/6f/3bd0007b64881a90a058c59a4869b1b4f130ddb86a726f884fafc67e5ef7/simplejson-3.20.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c34e028a2ba8553a208ded1da5fa8501833875078c4c00a50dffc33622057881", size = 138261 }, + { url = "https://files.pythonhosted.org/packages/15/5d/b6d0b71508e503c759a0a7563cb2c28716ec8af9828ca9f5b59023011406/simplejson-3.20.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b538f9d9e503b0dd43af60496780cb50755e4d8e5b34e5647b887675c1ae9fee", size = 146397 }, + { url = "https://files.pythonhosted.org/packages/19/24/40b3e5a3ca5e6f80cc1c639fcd5565ae087e72e8656dea780f02302ddc97/simplejson-3.20.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ab998e416ded6c58f549a22b6a8847e75a9e1ef98eb9fbb2863e1f9e61a4105b", size = 134020 }, + { url = "https://files.pythonhosted.org/packages/b9/8c/8fc2c2734ac9e514124635b25ca8f7e347db1ded4a30417ee41e78e6d61c/simplejson-3.20.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a8f1c307edf5fbf0c6db3396c5d3471409c4a40c7a2a466fbc762f20d46601a", size = 137598 }, + { url = "https://files.pythonhosted.org/packages/2e/d9/15036d7f43c6208fb0fbc827f9f897c1f577fba02aeb7a8a223581da4925/simplejson-3.20.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5a7bbac80bdb82a44303f5630baee140aee208e5a4618e8b9fde3fc400a42671", size = 139770 }, + { url = "https://files.pythonhosted.org/packages/73/cc/18374fb9dfcb4827b692ca5a33bdb607384ca06cdb645e0b863022dae8a3/simplejson-3.20.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:5ef70ec8fe1569872e5a3e4720c1e1dcb823879a3c78bc02589eb88fab920b1f", size = 139884 }, + { url = "https://files.pythonhosted.org/packages/5c/a2/1526d4152806670124dd499ff831726a92bd7e029e8349c4affa78ea8845/simplejson-3.20.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:cb11c09c99253a74c36925d461c86ea25f0140f3b98ff678322734ddc0f038d7", size = 148166 }, + { url = "https://files.pythonhosted.org/packages/a4/77/fc16d41b5f67a2591c9b6ff7b0f6aed2b2aed1b6912bb346b61279697638/simplejson-3.20.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:66f7c78c6ef776f8bd9afaad455e88b8197a51e95617bcc44b50dd974a7825ba", size = 140778 }, + { url = "https://files.pythonhosted.org/packages/4a/97/a26ef6b7387349623c042f329df70a4f3baf3a365fe6d1154d73da1dcf5a/simplejson-3.20.2-cp39-cp39-win32.whl", hash = "sha256:619ada86bfe3a5aa02b8222ca6bfc5aa3e1075c1fb5b3263d24ba579382df472", size = 74339 }, + { url = "https://files.pythonhosted.org/packages/dc/b7/94c6049a99e3c04eed2064e91295370b7429e2361188e35a78df562312e0/simplejson-3.20.2-cp39-cp39-win_amd64.whl", hash = "sha256:44a6235e09ca5cc41aa5870a952489c06aa4aee3361ae46daa947d8398e57502", size = 76067 }, + { url = "https://files.pythonhosted.org/packages/05/5b/83e1ff87eb60ca706972f7e02e15c0b33396e7bdbd080069a5d1b53cf0d8/simplejson-3.20.2-py3-none-any.whl", hash = "sha256:3b6bb7fb96efd673eac2e4235200bfffdc2353ad12c54117e1e4e2fc485ac017", size = 57309 }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614 }, +] + +[[package]] +name = "websocket-client" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/41/aa4bf9664e4cda14c3b39865b12251e8e7d239f4cd0e3cc1b6c2ccde25c1/websocket_client-1.9.0.tar.gz", hash = "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98", size = 70576 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616 }, +] From 9e40850bfaf1af283c5d17dbee28b28a3b66ede1 Mon Sep 17 00:00:00 2001 From: GenaroVogelius Date: Wed, 14 Jan 2026 10:29:28 -0300 Subject: [PATCH 8/9] fix: handle token update in both running and new event loops --- src/pyRofex/clients/rest_rfx.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/pyRofex/clients/rest_rfx.py b/src/pyRofex/clients/rest_rfx.py index 6d8ff51..18da01e 100644 --- a/src/pyRofex/clients/rest_rfx.py +++ b/src/pyRofex/clients/rest_rfx.py @@ -34,7 +34,31 @@ def __init__(self, environment, active_token=None): # Get the authentication Token. if not active_token: - asyncio.run(self.update_token()) + try: + loop = asyncio.get_running_loop() + except RuntimeError: + asyncio.run(self.update_token()) + else: + import concurrent.futures + import threading + + future = concurrent.futures.Future() + + def run_in_thread(): + new_loop = asyncio.new_event_loop() + asyncio.set_event_loop(new_loop) + try: + new_loop.run_until_complete(self.update_token()) + future.set_result(None) + except Exception as e: + future.set_exception(e) + finally: + new_loop.close() + + thread = threading.Thread(target=run_in_thread) + thread.start() + thread.join() + future.result() else: self.environment["token"] = active_token self.environment["initialized"] = True From a03007bd6dc925358d5e3d98968e17835644f5a4 Mon Sep 17 00:00:00 2001 From: GenaroVogelius Date: Wed, 14 Jan 2026 16:33:59 -0300 Subject: [PATCH 9/9] feat: implement synchronous handling of async calls with _run_async function --- src/pyRofex/service.py | 89 +++++++++++++++++++++++++++++------------- 1 file changed, 61 insertions(+), 28 deletions(-) diff --git a/src/pyRofex/service.py b/src/pyRofex/service.py index ccd0561..e3225a2 100644 --- a/src/pyRofex/service.py +++ b/src/pyRofex/service.py @@ -6,7 +6,9 @@ """ import asyncio +import concurrent.futures import logging +import threading from inspect import getfullargspec from .clients.rest_rfx import RestClient @@ -20,6 +22,37 @@ # ###################################################### +def _run_async(coro): + """Run an async coroutine, handling both cases: with and without a running event loop. + + :param coro: The coroutine to run. + :type coro: coroutine + :return: The result of the coroutine. + """ + try: + asyncio.get_running_loop() + except RuntimeError: + return asyncio.run(coro) + else: + future = concurrent.futures.Future() + + def run_in_thread(): + new_loop = asyncio.new_event_loop() + asyncio.set_event_loop(new_loop) + try: + result = new_loop.run_until_complete(coro) + future.set_result(result) + except Exception as e: + future.set_exception(e) + finally: + new_loop.close() + + thread = threading.Thread(target=run_in_thread) + thread.start() + thread.join() + return future.result() + + def initialize( user, password, @@ -150,7 +183,7 @@ def get_segments(environment=None): # Get the client for the environment and start the connection client: RestClient = globals.environment_config[environment]["rest_client"] - return asyncio.run(client.get_segments()) + return _run_async(client.get_segments()) def get_instruments(endpoint="all", environment=None, **kwargs): @@ -173,8 +206,8 @@ def get_instruments(endpoint="all", environment=None, **kwargs): _validate_initialization(environment) # Get the client for the environment and start the connection - client: RestClient = globals.environment_config[environment]["rest_client"] - return asyncio.run(client.get_instruments(endpoint, **kwargs)) + client: RestClient = globals.environment_config[environment]["rest_client"] + return _run_async(client.get_instruments(endpoint, **kwargs)) def get_all_instruments(environment=None): @@ -193,8 +226,8 @@ def get_all_instruments(environment=None): _validate_initialization(environment) # Get the client for the environment and make the request - client: RestClient = globals.environment_config[environment]["rest_client"] - return asyncio.run(client.get_all_instruments()) + client: RestClient = globals.environment_config[environment]["rest_client"] + return _run_async(client.get_all_instruments()) def get_detailed_instruments(environment=None): @@ -213,8 +246,8 @@ def get_detailed_instruments(environment=None): _validate_initialization(environment) # Get the client for the environment and make the request - client: RestClient = globals.environment_config[environment]["rest_client"] - return asyncio.run(client.get_detailed_instruments()) + client: RestClient = globals.environment_config[environment]["rest_client"] + return _run_async(client.get_detailed_instruments()) def get_instrument_details(ticker, market=Market.ROFEX, environment=None): @@ -237,8 +270,8 @@ def get_instrument_details(ticker, market=Market.ROFEX, environment=None): _validate_initialization(environment) # Get the client for the environment and make the request - client: RestClient = globals.environment_config[environment]["rest_client"] - return asyncio.run(client.get_instrument_details(ticker, market)) + client: RestClient = globals.environment_config[environment]["rest_client"] + return _run_async(client.get_instrument_details(ticker, market)) def get_market_data( @@ -269,8 +302,8 @@ def get_market_data( entries = _validate_market_data_entries(entries) # Get the client for the environment and make the request - client: RestClient = globals.environment_config[environment]["rest_client"] - return asyncio.run(client.get_market_data(ticker, entries, depth, market)) + client: RestClient = globals.environment_config[environment]["rest_client"] + return _run_async(client.get_market_data(ticker, entries, depth, market)) def get_order_status(client_order_id, proprietary=None, environment=None): @@ -297,8 +330,8 @@ def get_order_status(client_order_id, proprietary=None, environment=None): proprietary = globals.environment_config[environment]["proprietary"] # Get the client for the environment and make the request - client: RestClient = globals.environment_config[environment]["rest_client"] - return asyncio.run(client.get_order_status(client_order_id, proprietary)) + client: RestClient = globals.environment_config[environment]["rest_client"] + return _run_async(client.get_order_status(client_order_id, proprietary)) def send_order( @@ -361,8 +394,8 @@ def send_order( account = globals.environment_config[environment]["account"] # Get the client for the environment and make the request - client: RestClient = globals.environment_config[environment]["rest_client"] - return asyncio.run( + client: RestClient = globals.environment_config[environment]["rest_client"] + return _run_async( client.send_order( ticker, size, @@ -406,8 +439,8 @@ def cancel_order(client_order_id, proprietary=None, environment=None): proprietary = globals.environment_config[environment]["proprietary"] # Get the client for the environment and make the request - client: RestClient = globals.environment_config[environment]["rest_client"] - return asyncio.run(client.cancel_order(client_order_id, proprietary)) + client: RestClient = globals.environment_config[environment]["rest_client"] + return _run_async(client.cancel_order(client_order_id, proprietary)) def get_all_orders_status(account=None, environment=None): @@ -433,8 +466,8 @@ def get_all_orders_status(account=None, environment=None): account = globals.environment_config[environment]["account"] # Get the client for the environment and make the request - client: RestClient = globals.environment_config[environment]["rest_client"] - return asyncio.run(client.get_all_orders_by_account(account)) + client: RestClient = globals.environment_config[environment]["rest_client"] + return _run_async(client.get_all_orders_by_account(account)) def get_trade_history( @@ -463,8 +496,8 @@ def get_trade_history( _validate_initialization(environment) # Get the client for the environment and make the request - client: RestClient = globals.environment_config[environment]["rest_client"] - return asyncio.run(client.get_trade_history(ticker, start_date, end_date, market)) + client: RestClient = globals.environment_config[environment]["rest_client"] + return _run_async(client.get_trade_history(ticker, start_date, end_date, market)) def get_account_position(account=None, environment=None): @@ -490,8 +523,8 @@ def get_account_position(account=None, environment=None): account = globals.environment_config[environment]["account"] # Get the client for the environment and make the request - client: RestClient = globals.environment_config[environment]["rest_client"] - return asyncio.run(client.get_account_position(account)) + client: RestClient = globals.environment_config[environment]["rest_client"] + return _run_async(client.get_account_position(account)) def get_detailed_position(account=None, environment=None): @@ -517,8 +550,8 @@ def get_detailed_position(account=None, environment=None): account = globals.environment_config[environment]["account"] # Get the client for the environment and make the request - client: RestClient = globals.environment_config[environment]["rest_client"] - return asyncio.run(client.get_detailed_position(account)) + client: RestClient = globals.environment_config[environment]["rest_client"] + return _run_async(client.get_detailed_position(account)) def get_account_report(account=None, environment=None): @@ -545,7 +578,7 @@ def get_account_report(account=None, environment=None): # Get the client for the environment and make the request client: RestClient = globals.environment_config[environment]["rest_client"] - return asyncio.run(client.get_account_report(account)) + return _run_async(client.get_account_report(account)) # ###################################################### @@ -859,7 +892,7 @@ def cancel_order_via_websocket(client_order_id, proprietary=None, environment=No proprietary = globals.environment_config[environment]["proprietary"] # Get the client for the environment and make the request - client: WebSocketClient = globals.environment_config[environment]["ws_client"] + client: WebSocketClient = globals.environment_config[environment]["ws_client"] client.cancel_order(client_order_id, proprietary) @@ -922,7 +955,7 @@ def send_order_via_websocket( _validate_initialization(environment) # Gets the client for the environment - client: WebSocketClient = globals.environment_config[environment]["ws_client"] + client: WebSocketClient = globals.environment_config[environment]["ws_client"] # Checks the account and sets the default one if None is received. if account is None: