Skip to content

Commit e7c4755

Browse files
committed
Add uv support and update to python 3.11 minimum
Constructed to support both poetry and uv at the same time currently. Fixes #204
1 parent 071066a commit e7c4755

6 files changed

Lines changed: 143 additions & 52 deletions

File tree

.github/workflows/test.yml

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
name: ib_async
22

3-
on: [ push, pull_request ]
3+
on: [push, pull_request]
44

55
jobs:
6-
build:
7-
# https://github.com/actions/runner-images
6+
build-poetry:
7+
name: poetry (${{ matrix.python-version }})
88
runs-on: ubuntu-latest
99
strategy:
10+
fail-fast: false
1011
matrix:
11-
python-version: [ "3.10", "3.11", "3.12", "3.13", "3.14", "pypy3.10", "pypy3.11" ]
12+
python-version: ["3.11", "3.12", "3.13", "3.14", "pypy3.11"]
1213

1314
steps:
1415
- uses: actions/checkout@v4
@@ -18,11 +19,11 @@ jobs:
1819
with:
1920
python-version: ${{ matrix.python-version }}
2021

21-
- name: Install dependencies of dependencies
22+
- name: Install Poetry toolchain
2223
run: |
23-
pip install pip poetry uv setuptools wheel -U
24+
pip install pip poetry setuptools wheel -U
2425
25-
- name: Install dependencies
26+
- name: Install dependencies with Poetry
2627
run: |
2728
poetry install --with=dev
2829
@@ -34,3 +35,36 @@ jobs:
3435
- name: Ruff check
3536
run: |
3637
poetry run ruff check
38+
39+
build-uv:
40+
name: uv (${{ matrix.python-version }})
41+
runs-on: ubuntu-latest
42+
strategy:
43+
fail-fast: false
44+
matrix:
45+
python-version: ["3.11", "3.12", "3.13", "3.14", "pypy3.11"]
46+
47+
steps:
48+
- uses: actions/checkout@v4
49+
50+
- name: Set up Python ${{ matrix.python-version }}
51+
uses: actions/setup-python@v5
52+
with:
53+
python-version: ${{ matrix.python-version }}
54+
55+
- name: Install uv toolchain
56+
run: |
57+
pip install pip uv setuptools wheel -U
58+
59+
- name: Install dependencies with uv
60+
run: |
61+
uv sync --group dev
62+
63+
- name: MyPy static code analysis
64+
if: ${{ !startsWith(matrix.python-version, 'pypy') }}
65+
run: |
66+
uv run mypy --pretty ib_async
67+
68+
- name: Ruff check
69+
run: |
70+
uv run ruff check

README.md

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
[![Build](https://github.com/ib-api-reloaded/ib_async/actions/workflows/test.yml/badge.svg?branch=next)](https://github.com/ib-api-reloaded/ib_async/actions) [![PyVersion](https://img.shields.io/badge/python-3.10+-blue.svg)](#) <!-- [![Status](https://img.shields.io/badge/status-beta-green.svg)](#) --> [![PyPiVersion](https://img.shields.io/pypi/v/ib_async.svg)](https://pypi.python.org/pypi/ib_async) [![License](https://img.shields.io/badge/license-BSD-blue.svg)](#) <!-- [![Downloads](https://static.pepy.tech/badge/ib-insync)](https://pepy.tech/project/ib-insync) --> [![Docs](https://img.shields.io/badge/Documentation-green.svg)](https://ib-api-reloaded.github.io/ib_async/)
1+
[![Build](https://github.com/ib-api-reloaded/ib_async/actions/workflows/test.yml/badge.svg?branch=next)](https://github.com/ib-api-reloaded/ib_async/actions) [![PyVersion](https://img.shields.io/badge/python-3.11+-blue.svg)](#) <!-- [![Status](https://img.shields.io/badge/status-beta-green.svg)](#) --> [![PyPiVersion](https://img.shields.io/pypi/v/ib_async.svg)](https://pypi.python.org/pypi/ib_async) [![License](https://img.shields.io/badge/license-BSD-blue.svg)](#) <!-- [![Downloads](https://static.pepy.tech/badge/ib-insync)](https://pepy.tech/project/ib-insync) --> [![Docs](https://img.shields.io/badge/Documentation-green.svg)](https://ib-api-reloaded.github.io/ib_async/)
22

33
# ib_async
44

@@ -32,13 +32,29 @@ and the [API docs](https://ib-api-reloaded.github.io/ib_async/api.html).
3232

3333
## Installation
3434

35+
`ib_async` now targets Python 3.11+ and publishes standard PEP 621 metadata, so the project installs cleanly with `pip`, `uv`, and `poetry`.
36+
37+
### Install with pip
38+
3539
```
3640
pip install ib_async
3741
```
3842

43+
### Install with uv
44+
45+
```
46+
uv add ib_async
47+
```
48+
49+
### Install with poetry
50+
51+
```
52+
poetry add ib_async
53+
```
54+
3955
Requirements:
4056

41-
- Python 3.10 or higher
57+
- Python 3.11 or higher
4258
- We plan to support Python releases [2 years back](https://devguide.python.org/versions/) which allows us to continue adding newer features and performance improvements over time.
4359
- A running IB Gateway application (or TWS with API mode enabled)
4460
- [stable gateway](https://www.interactivebrokers.com/en/trading/ibgateway-stable.php) — updated every few months
@@ -50,41 +66,59 @@ The ibapi package from IB is not needed. `ib_async` implements the full IBKR API
5066

5167
## Build Manually
5268

53-
First, install poetry:
69+
First, install your preferred environment manager:
5470

5571
```
5672
pip install poetry -U
5773
```
5874

59-
### Installing Only Library
75+
or:
76+
77+
```
78+
pip install uv -U
79+
```
80+
81+
### Install the project with poetry
6082

6183
```
6284
poetry install
6385
```
6486

65-
### Install Everything (enable docs + dev testing)
87+
### Install the project with uv
6688

6789
```
90+
uv sync
91+
```
92+
93+
### Install everything for development and docs
94+
95+
```bash
6896
poetry install --with=docs,dev
97+
uv sync --group dev --group docs
6998
```
7099

71100
## Generate Docs
72101

73-
```
102+
```bash
74103
poetry install --with=docs
75104
poetry run sphinx-build -b html docs html
105+
106+
uv sync --group docs
107+
uv run sphinx-build -b html docs html
76108
```
77109

78110
## Check Types
79111

80-
```
112+
```bash
81113
poetry run mypy ib_async
114+
uv run mypy ib_async
82115
```
83116

84117
## Build Package
85118

86-
```
119+
```bash
87120
poetry build
121+
python -m build
88122
```
89123

90124
## Upload Package (if maintaining)
@@ -481,19 +515,26 @@ The complete [API documentation](https://ib-api-reloaded.github.io/ib_async/api.
481515
```bash
482516
poetry install --with=dev
483517
poetry run pytest
518+
519+
uv sync --group dev
520+
uv run pytest
484521
```
485522

486523
### Type Checking
487524

488525
```bash
489526
poetry run mypy ib_async
527+
uv run mypy ib_async
490528
```
491529

492530
### Code Formatting
493531

494532
```bash
495533
poetry run ruff format
496534
poetry run ruff check --fix
535+
536+
uv run ruff format
537+
uv run ruff check --fix
497538
```
498539

499540
### Local Development
@@ -507,6 +548,7 @@ cd ib_async
507548
2. Install dependencies:
508549
```bash
509550
poetry install --with=dev,docs
551+
uv sync --group dev --group docs
510552
```
511553

512554
3. Make your changes and run tests:

ib_async/ib.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -457,7 +457,7 @@ def waitOnUpdate(self, timeout: float = 0) -> bool:
457457
if timeout:
458458
try:
459459
util.run(asyncio.wait_for(self.updateEvent, timeout))
460-
except asyncio.TimeoutError:
460+
except TimeoutError:
461461
return False
462462
else:
463463
util.run(self.updateEvent)
@@ -2096,7 +2096,7 @@ async def connectAsync(
20962096
if fetchFields & StartupFetch.EXECUTIONS:
20972097
try:
20982098
await asyncio.wait_for(self.reqExecutionsAsync(), timeout)
2099-
except asyncio.TimeoutError:
2099+
except TimeoutError:
21002100
msg = "executions request timed out"
21012101
errors.append(msg)
21022102
self._logger.error(msg)
@@ -2315,7 +2315,7 @@ async def reqMatchingSymbolsAsync(
23152315
try:
23162316
await asyncio.wait_for(future, 4)
23172317
return future.result()
2318-
except asyncio.TimeoutError:
2318+
except TimeoutError:
23192319
self._logger.error("reqMatchingSymbolsAsync: Timeout")
23202320
return None
23212321

@@ -2327,7 +2327,7 @@ async def reqMarketRuleAsync(
23272327
self.client.reqMarketRule(marketRuleId)
23282328
await asyncio.wait_for(future, 1)
23292329
return future.result()
2330-
except asyncio.TimeoutError:
2330+
except TimeoutError:
23312331
self._logger.error("reqMarketRuleAsync: Timeout")
23322332
return None
23332333

@@ -2375,7 +2375,7 @@ async def reqHistoricalDataAsync(
23752375
task = asyncio.wait_for(future, timeout) if timeout else future
23762376
try:
23772377
await task
2378-
except asyncio.TimeoutError:
2378+
except TimeoutError:
23792379
self.client.cancelHistoricalData(reqId)
23802380
self._logger.warning(f"reqHistoricalData: Timeout for {contract}")
23812381
bars.clear()
@@ -2520,7 +2520,7 @@ async def calculateImpliedVolatilityAsync(
25202520
try:
25212521
await asyncio.wait_for(future, 4)
25222522
return future.result()
2523-
except asyncio.TimeoutError:
2523+
except TimeoutError:
25242524
self._logger.error("calculateImpliedVolatilityAsync: Timeout")
25252525
return None
25262526
finally:
@@ -2541,7 +2541,7 @@ async def calculateOptionPriceAsync(
25412541
try:
25422542
await asyncio.wait_for(future, 4)
25432543
return future.result()
2544-
except asyncio.TimeoutError:
2544+
except TimeoutError:
25452545
self._logger.error("calculateOptionPriceAsync: Timeout")
25462546
return None
25472547
finally:
@@ -2596,7 +2596,7 @@ async def reqHistoricalNewsAsync(
25962596
try:
25972597
await asyncio.wait_for(future, 4)
25982598
return future.result()
2599-
except asyncio.TimeoutError:
2599+
except TimeoutError:
26002600
self._logger.error("reqHistoricalNewsAsync: Timeout")
26012601
return None
26022602

@@ -2606,7 +2606,7 @@ async def requestFAAsync(self, faDataType: int):
26062606
try:
26072607
await asyncio.wait_for(future, 4)
26082608
return future.result()
2609-
except asyncio.TimeoutError:
2609+
except TimeoutError:
26102610
self._logger.error("requestFAAsync: Timeout")
26112611

26122612
async def getWshMetaDataAsync(self) -> str:

ib_async/objects.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
from __future__ import annotations
44

55
from dataclasses import dataclass, field
6+
from datetime import UTC, datetime, tzinfo
67
from datetime import date as date_
7-
from datetime import datetime, timezone, tzinfo
88
from typing import Any, NamedTuple
99

1010
from eventkit import Event
@@ -591,4 +591,4 @@ class IBDefaults:
591591
unset: Any = nan
592592

593593
# optionally change the timezone used for log history events in objects (no impact on orders or data processing)
594-
timezone: tzinfo = timezone.utc
594+
timezone: tzinfo = UTC

ib_async/util.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
Event to emit global exceptions.
2424
"""
2525

26-
EPOCH: Final = dt.datetime(1970, 1, 1, tzinfo=dt.timezone.utc)
26+
EPOCH: Final = dt.datetime(1970, 1, 1, tzinfo=dt.UTC)
2727
UNSET_INTEGER: Final = 2**31 - 1
2828
UNSET_DOUBLE: Final = sys.float_info.max
2929

@@ -421,7 +421,7 @@ def timeRange(start: Time_t, end: Time_t, step: float) -> Iterator[dt.datetime]:
421421
assert step > 0
422422
delta = dt.timedelta(seconds=step)
423423
t = _fillDate(start)
424-
tz = dt.timezone.utc if t.tzinfo else None
424+
tz = dt.UTC if t.tzinfo else None
425425
now = dt.datetime.now(tz)
426426
while t < now:
427427
t += delta
@@ -454,7 +454,7 @@ async def timeRangeAsync(
454454

455455
delta = dt.timedelta(seconds=step)
456456
t = _fillDate(start)
457-
tz = dt.timezone.utc if t.tzinfo else None
457+
tz = dt.UTC if t.tzinfo else None
458458
now = dt.datetime.now(tz)
459459
while t < now:
460460
t += delta
@@ -577,11 +577,11 @@ def formatIBDatetime(t: dt.date | dt.datetime | str | None) -> str:
577577
s = ""
578578
elif isinstance(t, dt.datetime):
579579
# convert to UTC timezone
580-
t = t.astimezone(tz=dt.timezone.utc)
580+
t = t.astimezone(tz=dt.UTC)
581581
s = t.strftime("%Y%m%d %H:%M:%S UTC")
582582
elif isinstance(t, dt.date):
583583
t = dt.datetime(t.year, t.month, t.day, 23, 59, 59).astimezone(
584-
tz=dt.timezone.utc
584+
tz=dt.UTC
585585
)
586586
s = t.strftime("%Y%m%d %H:%M:%S UTC")
587587
else:
@@ -599,7 +599,7 @@ def parseIBDatetime(s: str) -> dt.date | dt.datetime:
599599
d = int(s[6:8])
600600
t = dt.date(y, m, d)
601601
elif s.isdigit():
602-
t = dt.datetime.fromtimestamp(int(s), dt.timezone.utc)
602+
t = dt.datetime.fromtimestamp(int(s), dt.UTC)
603603
elif s.count(" ") >= 2 and " " not in s:
604604
# 20221125 10:00:00 Europe/Amsterdam
605605
s0, s1, s2 = s.split(" ", 2)

0 commit comments

Comments
 (0)