Compare commits
3 Commits
08d159e652
...
b30372f4fe
| Author | SHA1 | Date |
|---|---|---|
|
|
b30372f4fe | |
|
|
60b4526fad | |
|
|
c04cc0e87f |
|
|
@ -20,10 +20,14 @@ Handy cross-broker utils.
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
# from functools import partial
|
# from functools import partial
|
||||||
|
from typing import (
|
||||||
|
Type,
|
||||||
|
)
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import httpx
|
import httpx
|
||||||
import logging
|
import logging
|
||||||
|
from msgspec import Struct
|
||||||
|
|
||||||
from piker.log import (
|
from piker.log import (
|
||||||
colorize_json,
|
colorize_json,
|
||||||
|
|
@ -97,6 +101,12 @@ class DataThrottle(BrokerError):
|
||||||
'''
|
'''
|
||||||
# TODO: add in throttle metrics/feedback
|
# TODO: add in throttle metrics/feedback
|
||||||
|
|
||||||
|
class SchemaMismatch(BrokerError):
|
||||||
|
'''
|
||||||
|
Market `Pair` fields mismatch, likely due to provider API update.
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
def resproc(
|
def resproc(
|
||||||
resp: httpx.Response,
|
resp: httpx.Response,
|
||||||
|
|
@ -123,3 +133,45 @@ def resproc(
|
||||||
log.debug(f"Received json contents:\n{colorize_json(msg)}")
|
log.debug(f"Received json contents:\n{colorize_json(msg)}")
|
||||||
|
|
||||||
return msg if return_json else resp
|
return msg if return_json else resp
|
||||||
|
|
||||||
|
|
||||||
|
def get_or_raise_on_pair_schema_mismatch(
|
||||||
|
pair_type: Type[Struct],
|
||||||
|
fields_data: dict,
|
||||||
|
provider_name: str,
|
||||||
|
api_url: str|None = None,
|
||||||
|
) -> Struct:
|
||||||
|
'''
|
||||||
|
Boilerplate helper around assset-`Pair` field schema mismatches,
|
||||||
|
normally due to provider API updates.
|
||||||
|
|
||||||
|
'''
|
||||||
|
try:
|
||||||
|
pair: Struct = pair_type(**fields_data)
|
||||||
|
return pair
|
||||||
|
except TypeError as err:
|
||||||
|
|
||||||
|
from tractor.devx.pformat import ppfmt
|
||||||
|
repr_data: str = ppfmt(fields_data)
|
||||||
|
report: str = (
|
||||||
|
f'Field mismatch we need to codify!\n'
|
||||||
|
f'\n'
|
||||||
|
f'{pair_type!r}({repr_data})'
|
||||||
|
f'\n'
|
||||||
|
f'^^^ {err.args[0]!r} ^^^\n'
|
||||||
|
f'\n'
|
||||||
|
f"Don't panic, prolly {provider_name!r} "
|
||||||
|
f"changed their symbology schema..\n"
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
api_url
|
||||||
|
or
|
||||||
|
(api_url := pair_type._api_url)
|
||||||
|
):
|
||||||
|
report += (
|
||||||
|
f'\n'
|
||||||
|
f'Check out their API docs here:\n'
|
||||||
|
f'{api_url}\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
raise SchemaMismatch(report) from err
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,9 @@ from piker import config
|
||||||
from piker.clearing._messages import (
|
from piker.clearing._messages import (
|
||||||
Order,
|
Order,
|
||||||
)
|
)
|
||||||
|
from piker.brokers._util import (
|
||||||
|
get_or_raise_on_pair_schema_mismatch,
|
||||||
|
)
|
||||||
from piker.accounting import (
|
from piker.accounting import (
|
||||||
Asset,
|
Asset,
|
||||||
digits_to_dec,
|
digits_to_dec,
|
||||||
|
|
@ -370,20 +373,12 @@ class Client:
|
||||||
item['filters'] = filters
|
item['filters'] = filters
|
||||||
|
|
||||||
pair_type: Type = PAIRTYPES[venue]
|
pair_type: Type = PAIRTYPES[venue]
|
||||||
try:
|
pair: Pair = get_or_raise_on_pair_schema_mismatch(
|
||||||
pair: Pair = pair_type(**item)
|
pair_type=pair_type,
|
||||||
except Exception as e:
|
fields_data=item,
|
||||||
e.add_note(
|
provider_name='binance',
|
||||||
f'\n'
|
api_url='https://binance-docs.github.io/apidocs/spot/en/#exchange-information',
|
||||||
f'New or removed field we need to codify!\n'
|
)
|
||||||
f'pair-type: {pair_type!r}\n'
|
|
||||||
f'\n'
|
|
||||||
f"Don't panic, prolly stupid binance changed their symbology schema again..\n"
|
|
||||||
f'Check out their API docs here:\n'
|
|
||||||
f'\n'
|
|
||||||
f'https://binance-docs.github.io/apidocs/spot/en/#exchange-information\n'
|
|
||||||
)
|
|
||||||
raise
|
|
||||||
pair_table[pair.symbol.upper()] = pair
|
pair_table[pair.symbol.upper()] = pair
|
||||||
|
|
||||||
# update an additional top-level-cross-venue-table
|
# update an additional top-level-cross-venue-table
|
||||||
|
|
@ -581,8 +576,8 @@ class Client:
|
||||||
self,
|
self,
|
||||||
mkt: MktPair,
|
mkt: MktPair,
|
||||||
|
|
||||||
start_dt: datetime | None = None,
|
start_dt: datetime|None = None,
|
||||||
end_dt: datetime | None = None,
|
end_dt: datetime|None = None,
|
||||||
|
|
||||||
as_np: bool = True,
|
as_np: bool = True,
|
||||||
|
|
||||||
|
|
@ -609,7 +604,11 @@ class Client:
|
||||||
start_time = binance_timestamp(start_dt)
|
start_time = binance_timestamp(start_dt)
|
||||||
end_time = binance_timestamp(end_dt)
|
end_time = binance_timestamp(end_dt)
|
||||||
|
|
||||||
bs_pair: Pair = self._pairs[mkt.bs_fqme.upper()]
|
import tractor
|
||||||
|
with tractor.devx.maybe_open_crash_handler():
|
||||||
|
bs_pair: Pair = self._pairs[
|
||||||
|
mkt.bs_fqme.upper()
|
||||||
|
]
|
||||||
|
|
||||||
# https://binance-docs.github.io/apidocs/spot/en/#kline-candlestick-data
|
# https://binance-docs.github.io/apidocs/spot/en/#kline-candlestick-data
|
||||||
bars = await self._api(
|
bars = await self._api(
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ import tractor
|
||||||
from piker.brokers import (
|
from piker.brokers import (
|
||||||
open_cached_client,
|
open_cached_client,
|
||||||
NoData,
|
NoData,
|
||||||
|
SymbolNotFound,
|
||||||
)
|
)
|
||||||
from piker._cacheables import (
|
from piker._cacheables import (
|
||||||
async_lifo_cache,
|
async_lifo_cache,
|
||||||
|
|
@ -305,6 +306,10 @@ async def get_mkt_info(
|
||||||
|
|
||||||
# uppercase since kraken bs_mktid is always upper
|
# uppercase since kraken bs_mktid is always upper
|
||||||
if 'binance' not in fqme.lower():
|
if 'binance' not in fqme.lower():
|
||||||
|
log.warning(
|
||||||
|
f'Missing `.<provider>` part in fqme ??\n'
|
||||||
|
f'fqme: {fqme!r}\n'
|
||||||
|
)
|
||||||
fqme += '.binance'
|
fqme += '.binance'
|
||||||
|
|
||||||
mkt_mode: str = ''
|
mkt_mode: str = ''
|
||||||
|
|
@ -319,6 +324,12 @@ async def get_mkt_info(
|
||||||
venue: str = venue.upper()
|
venue: str = venue.upper()
|
||||||
venue_lower: str = venue.lower()
|
venue_lower: str = venue.lower()
|
||||||
|
|
||||||
|
if not venue:
|
||||||
|
raise SymbolNotFound(
|
||||||
|
f'Invalid or missing .<venue> part in fqme?\n'
|
||||||
|
f'fqme: {fqme!r}\n'
|
||||||
|
)
|
||||||
|
|
||||||
# XXX TODO: we should change the usdtm_futes name to just
|
# XXX TODO: we should change the usdtm_futes name to just
|
||||||
# usdm_futes (dropping the tether part) since it turns out that
|
# usdm_futes (dropping the tether part) since it turns out that
|
||||||
# there are indeed USD-tokens OTHER THEN tether being used as
|
# there are indeed USD-tokens OTHER THEN tether being used as
|
||||||
|
|
@ -360,6 +371,8 @@ async def get_mkt_info(
|
||||||
if not mkt_mode:
|
if not mkt_mode:
|
||||||
mkt_mode: str = f'{venue_lower}_futes'
|
mkt_mode: str = f'{venue_lower}_futes'
|
||||||
|
|
||||||
|
await tractor.pause()
|
||||||
|
|
||||||
async with open_cached_client(
|
async with open_cached_client(
|
||||||
'binance',
|
'binance',
|
||||||
) as client:
|
) as client:
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ Per market data-type definitions and schemas types.
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import (
|
from typing import (
|
||||||
|
ClassVar,
|
||||||
Literal,
|
Literal,
|
||||||
)
|
)
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
@ -203,6 +204,8 @@ class FutesPair(Pair):
|
||||||
# NOTE: see `.data._symcache.SymbologyCache.load()` for why
|
# NOTE: see `.data._symcache.SymbologyCache.load()` for why
|
||||||
ns_path: str = 'piker.brokers.binance:FutesPair'
|
ns_path: str = 'piker.brokers.binance:FutesPair'
|
||||||
|
|
||||||
|
_api_url: ClassVar[str] = 'https://binance-docs.github.io/apidocs/spot/en/#exchange-information'
|
||||||
|
|
||||||
# NOTE: for compat with spot pairs and `MktPair.src: Asset`
|
# NOTE: for compat with spot pairs and `MktPair.src: Asset`
|
||||||
# processing..
|
# processing..
|
||||||
@property
|
@property
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,7 @@ from .symbols import (
|
||||||
)
|
)
|
||||||
from ...log import get_logger
|
from ...log import get_logger
|
||||||
from .venues import (
|
from .venues import (
|
||||||
|
is_expired,
|
||||||
is_venue_open,
|
is_venue_open,
|
||||||
sesh_times,
|
sesh_times,
|
||||||
is_venue_closure,
|
is_venue_closure,
|
||||||
|
|
@ -496,7 +497,7 @@ class Client:
|
||||||
await self.ib.reqContractDetailsAsync(contract)
|
await self.ib.reqContractDetailsAsync(contract)
|
||||||
)[0]
|
)[0]
|
||||||
# convert to makt-native tz
|
# convert to makt-native tz
|
||||||
tz: str = details.timeZoneId
|
tz: str = details.timeZoneId or 'EST'
|
||||||
end_dt = end_dt.in_tz(tz)
|
end_dt = end_dt.in_tz(tz)
|
||||||
first_dt: DateTime = from_timestamp(first).in_tz(tz)
|
first_dt: DateTime = from_timestamp(first).in_tz(tz)
|
||||||
last_dt: DateTime = from_timestamp(last).in_tz(tz)
|
last_dt: DateTime = from_timestamp(last).in_tz(tz)
|
||||||
|
|
@ -508,10 +509,18 @@ class Client:
|
||||||
_open_now: bool = is_venue_open(
|
_open_now: bool = is_venue_open(
|
||||||
con_deats=details,
|
con_deats=details,
|
||||||
)
|
)
|
||||||
|
_is_expired: bool = is_expired(
|
||||||
|
con_deats=details,
|
||||||
|
)
|
||||||
|
|
||||||
# XXX, do gap detections.
|
# XXX, do gap detections.
|
||||||
has_closure_gap: bool = False
|
has_closure_gap: bool = False
|
||||||
if (
|
if (
|
||||||
|
# XXX, expired tracts can't be introspected
|
||||||
|
# for open/closure intervals due to ib's chitty
|
||||||
|
# details seemingly..
|
||||||
|
not _is_expired
|
||||||
|
and
|
||||||
last_dt.add(seconds=sample_period_s)
|
last_dt.add(seconds=sample_period_s)
|
||||||
<
|
<
|
||||||
end_dt
|
end_dt
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ from typing import (
|
||||||
|
|
||||||
import exchange_calendars as xcals
|
import exchange_calendars as xcals
|
||||||
from pendulum import (
|
from pendulum import (
|
||||||
|
parse,
|
||||||
now,
|
now,
|
||||||
Duration,
|
Duration,
|
||||||
Interval,
|
Interval,
|
||||||
|
|
@ -56,6 +57,17 @@ if TYPE_CHECKING:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def is_expired(
|
||||||
|
con_deats: ContractDetails,
|
||||||
|
) -> bool:
|
||||||
|
'''
|
||||||
|
Predicate
|
||||||
|
|
||||||
|
'''
|
||||||
|
expiry_dt: datetime = parse(con_deats.realExpirationDate)
|
||||||
|
return expiry_dt.date() >= now().date()
|
||||||
|
|
||||||
|
|
||||||
def has_weekend(
|
def has_weekend(
|
||||||
period: Interval,
|
period: Interval,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
|
|
@ -170,7 +182,22 @@ def sesh_times(
|
||||||
get the (day-agnostic) times for the start/end.
|
get the (day-agnostic) times for the start/end.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
earliest_sesh: Interval = next(iter_sessions(con_deats))
|
# ?TODO, lookup the next front contract instead?
|
||||||
|
if is_expired(con_deats):
|
||||||
|
raise ValueError(
|
||||||
|
f'Contract is already expired!\n'
|
||||||
|
f'Choose an active alt contract instead.\n'
|
||||||
|
f'con_deats: {con_deats!r}\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
maybe_sessions: list[Interval] = list(iter_sessions(con_deats))
|
||||||
|
if not maybe_sessions:
|
||||||
|
raise ValueError(
|
||||||
|
f'Contract has no trading-session info?\n'
|
||||||
|
f'con_deats: {con_deats!r}\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
earliest_sesh: Interval = maybe_sessions[0]
|
||||||
return (
|
return (
|
||||||
earliest_sesh.start.time(),
|
earliest_sesh.start.time(),
|
||||||
earliest_sesh.end.time(),
|
earliest_sesh.end.time(),
|
||||||
|
|
@ -211,7 +238,13 @@ def is_venue_closure(
|
||||||
'''
|
'''
|
||||||
open: Time
|
open: Time
|
||||||
close: Time
|
close: Time
|
||||||
open, close = sesh_times(con_deats)
|
maybe_oc: tuple|None = sesh_times(con_deats)
|
||||||
|
if maybe_oc is None:
|
||||||
|
# XXX, should never get here.
|
||||||
|
breakpoint()
|
||||||
|
return False
|
||||||
|
|
||||||
|
open, close = maybe_oc
|
||||||
|
|
||||||
# ensure times are in mkt-native timezone
|
# ensure times are in mkt-native timezone
|
||||||
tz: str = con_deats.timeZoneId
|
tz: str = con_deats.timeZoneId
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ from piker.brokers._util import (
|
||||||
SymbolNotFound,
|
SymbolNotFound,
|
||||||
BrokerError,
|
BrokerError,
|
||||||
DataThrottle,
|
DataThrottle,
|
||||||
|
get_or_raise_on_pair_schema_mismatch,
|
||||||
)
|
)
|
||||||
from piker.accounting import Transaction
|
from piker.accounting import Transaction
|
||||||
from piker.log import get_logger
|
from piker.log import get_logger
|
||||||
|
|
@ -502,7 +503,16 @@ class Client:
|
||||||
|
|
||||||
# NOTE: always cache in pairs tables for faster lookup
|
# NOTE: always cache in pairs tables for faster lookup
|
||||||
with tractor.devx.maybe_open_crash_handler(): # as bxerr:
|
with tractor.devx.maybe_open_crash_handler(): # as bxerr:
|
||||||
pair = Pair(xname=xkey, **data)
|
# pair = Pair(xname=xkey, **data)
|
||||||
|
pair: Pair = get_or_raise_on_pair_schema_mismatch(
|
||||||
|
pair_type=Pair,
|
||||||
|
fields_data=dict(
|
||||||
|
xname=xkey,
|
||||||
|
**data,
|
||||||
|
),
|
||||||
|
provider_name='kraken',
|
||||||
|
# api_url='https://binance-docs.github.io/apidocs/spot/en/#exchange-information',
|
||||||
|
)
|
||||||
|
|
||||||
# register the above `Pair` structs for all
|
# register the above `Pair` structs for all
|
||||||
# key-sets/monikers: a set of 4 (frickin) tables
|
# key-sets/monikers: a set of 4 (frickin) tables
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,9 @@ Symbology defs and search.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
from typing import (
|
||||||
|
ClassVar,
|
||||||
|
)
|
||||||
|
|
||||||
import tractor
|
import tractor
|
||||||
|
|
||||||
|
|
@ -86,9 +89,14 @@ class Pair(Struct):
|
||||||
short_position_limit: float = 0
|
short_position_limit: float = 0
|
||||||
long_position_limit: float = float('inf')
|
long_position_limit: float = float('inf')
|
||||||
|
|
||||||
|
# TODO, add API note when this was added!
|
||||||
|
# execution_venue: str|None = None
|
||||||
|
|
||||||
# TODO: should we make this a literal NamespacePath ref?
|
# TODO: should we make this a literal NamespacePath ref?
|
||||||
ns_path: str = 'piker.brokers.kraken:Pair'
|
ns_path: str = 'piker.brokers.kraken:Pair'
|
||||||
|
|
||||||
|
_api_url: ClassVar[str] = 'https://docs.kraken.com/api/docs/rest-api/get-tradable-asset-pairs'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def bs_mktid(self) -> str:
|
def bs_mktid(self) -> str:
|
||||||
'''
|
'''
|
||||||
|
|
|
||||||
|
|
@ -206,8 +206,8 @@ pyvnc = { git = "https://github.com/regulad/pyvnc.git" }
|
||||||
# xonsh = { git = 'https://github.com/xonsh/xonsh.git', branch = 'main' }
|
# xonsh = { git = 'https://github.com/xonsh/xonsh.git', branch = 'main' }
|
||||||
|
|
||||||
# XXX since, we're like, always hacking new shite all-the-time. Bp
|
# XXX since, we're like, always hacking new shite all-the-time. Bp
|
||||||
tractor = { git = "https://github.com/goodboy/tractor.git", branch ="main" }
|
# tractor = { git = "https://github.com/goodboy/tractor.git", branch ="main" }
|
||||||
# tractor = { git = "https://pikers.dev/goodboy/tractor", branch = "piker_pin" }
|
# tractor = { git = "https://pikers.dev/goodboy/tractor", branch = "piker_pin" }
|
||||||
# ------ goodboy ------
|
# ------ goodboy ------
|
||||||
# hackin dev-envs, usually there's something new he's hackin in..
|
# hackin dev-envs, usually there's something new he's hackin in..
|
||||||
# tractor = { path = "../tractor", editable = true }
|
tractor = { path = "../tractor", editable = true }
|
||||||
|
|
|
||||||
46
uv.lock
46
uv.lock
|
|
@ -1034,7 +1034,7 @@ requires-dist = [
|
||||||
{ name = "tomli", specifier = ">=2.0.1,<3.0.0" },
|
{ name = "tomli", specifier = ">=2.0.1,<3.0.0" },
|
||||||
{ name = "tomli-w", specifier = ">=1.0.0,<2.0.0" },
|
{ name = "tomli-w", specifier = ">=1.0.0,<2.0.0" },
|
||||||
{ name = "tomlkit", git = "https://github.com/pikers/tomlkit.git?branch=piker_pin" },
|
{ name = "tomlkit", git = "https://github.com/pikers/tomlkit.git?branch=piker_pin" },
|
||||||
{ name = "tractor", git = "https://github.com/goodboy/tractor.git?branch=main" },
|
{ name = "tractor", editable = "../tractor" },
|
||||||
{ name = "trio", specifier = ">=0.27" },
|
{ name = "trio", specifier = ">=0.27" },
|
||||||
{ name = "trio-typing", specifier = ">=0.10.0" },
|
{ name = "trio-typing", specifier = ">=0.10.0" },
|
||||||
{ name = "trio-util", specifier = ">=0.7.0,<0.8.0" },
|
{ name = "trio-util", specifier = ">=0.7.0,<0.8.0" },
|
||||||
|
|
@ -1680,7 +1680,7 @@ wheels = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tractor"
|
name = "tractor"
|
||||||
version = "0.1.0a6.dev0"
|
version = "0.1.0a6.dev0"
|
||||||
source = { git = "https://github.com/goodboy/tractor.git?branch=main#e77198bb64f0467a50e251ed140daee439752354" }
|
source = { editable = "../tractor" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "bidict" },
|
{ name = "bidict" },
|
||||||
{ name = "cffi" },
|
{ name = "cffi" },
|
||||||
|
|
@ -1693,6 +1693,48 @@ dependencies = [
|
||||||
{ name = "wrapt" },
|
{ name = "wrapt" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
requires-dist = [
|
||||||
|
{ name = "bidict", specifier = ">=0.23.1" },
|
||||||
|
{ name = "cffi", specifier = ">=1.17.1" },
|
||||||
|
{ name = "colorlog", specifier = ">=6.8.2,<7" },
|
||||||
|
{ name = "msgspec", specifier = ">=0.19.0" },
|
||||||
|
{ name = "pdbp", specifier = ">=1.8.2,<2" },
|
||||||
|
{ name = "platformdirs", specifier = ">=4.4.0" },
|
||||||
|
{ name = "tricycle", specifier = ">=0.4.1,<0.5" },
|
||||||
|
{ name = "trio", specifier = ">0.27" },
|
||||||
|
{ name = "wrapt", specifier = ">=1.16.0,<2" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata.requires-dev]
|
||||||
|
dev = [
|
||||||
|
{ name = "greenback", specifier = ">=1.2.1,<2" },
|
||||||
|
{ name = "pexpect", specifier = ">=4.9.0,<5" },
|
||||||
|
{ name = "prompt-toolkit", specifier = ">=3.0.50" },
|
||||||
|
{ name = "psutil", specifier = ">=7.0.0" },
|
||||||
|
{ name = "pyperclip", specifier = ">=1.9.0" },
|
||||||
|
{ name = "pytest", specifier = ">=8.3.5" },
|
||||||
|
{ name = "stackscope", specifier = ">=0.2.2,<0.3" },
|
||||||
|
{ name = "typing-extensions", specifier = ">=4.14.1" },
|
||||||
|
{ name = "xonsh", specifier = ">=0.22.2" },
|
||||||
|
]
|
||||||
|
devx = [
|
||||||
|
{ name = "greenback", specifier = ">=1.2.1,<2" },
|
||||||
|
{ name = "stackscope", specifier = ">=0.2.2,<0.3" },
|
||||||
|
{ name = "typing-extensions", specifier = ">=4.14.1" },
|
||||||
|
]
|
||||||
|
lint = [{ name = "ruff", specifier = ">=0.9.6" }]
|
||||||
|
repl = [
|
||||||
|
{ name = "prompt-toolkit", specifier = ">=3.0.50" },
|
||||||
|
{ name = "psutil", specifier = ">=7.0.0" },
|
||||||
|
{ name = "pyperclip", specifier = ">=1.9.0" },
|
||||||
|
{ name = "xonsh", specifier = ">=0.22.2" },
|
||||||
|
]
|
||||||
|
testing = [
|
||||||
|
{ name = "pexpect", specifier = ">=4.9.0,<5" },
|
||||||
|
{ name = "pytest", specifier = ">=8.3.5" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tricycle"
|
name = "tricycle"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue