Skip to content

Commit 34cf7c4

Browse files
authored
Merge pull request #130 from okxapi/dev-add-cicd
feat: add ci cd
2 parents 892bc6b + ee04e6b commit 34cf7c4

4 files changed

Lines changed: 154 additions & 43 deletions

File tree

.github/workflows/ci.yml

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# GitHub Actions CI/CD Configuration for python-okx
2+
name: CI
3+
4+
on:
5+
push:
6+
branches:
7+
- master
8+
- 'release/*'
9+
- 'releases/*'
10+
pull_request:
11+
branches:
12+
- master
13+
- 'release/*'
14+
- 'releases/*'
15+
16+
jobs:
17+
# ============================================
18+
# LINT JOB
19+
# ============================================
20+
lint:
21+
name: Lint
22+
runs-on: ubuntu-latest
23+
steps:
24+
- uses: actions/checkout@v4
25+
26+
- name: Set up Python
27+
uses: actions/setup-python@v5
28+
with:
29+
python-version: "3.11"
30+
31+
- name: Install linting tools
32+
run: |
33+
python -m pip install --upgrade pip
34+
pip install flake8
35+
36+
- name: Run flake8
37+
run: |
38+
flake8 okx/ --max-line-length=120 --ignore=E501,W503,E203 --count --show-source --statistics
39+
continue-on-error: true # Set to false once codebase is cleaned up
40+
41+
# ============================================
42+
# TEST JOB
43+
# ============================================
44+
test:
45+
name: Test (Python ${{ matrix.python-version }})
46+
runs-on: ubuntu-latest
47+
strategy:
48+
fail-fast: false
49+
matrix:
50+
python-version: ["3.9", "3.10", "3.11", "3.12"]
51+
52+
steps:
53+
- uses: actions/checkout@v4
54+
55+
- name: Set up Python ${{ matrix.python-version }}
56+
uses: actions/setup-python@v5
57+
with:
58+
python-version: ${{ matrix.python-version }}
59+
60+
- name: Cache pip dependencies
61+
uses: actions/cache@v4
62+
with:
63+
path: ~/.cache/pip
64+
key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('setup.py') }}
65+
restore-keys: |
66+
${{ runner.os }}-pip-${{ matrix.python-version }}-
67+
${{ runner.os }}-pip-
68+
69+
- name: Install dependencies
70+
run: |
71+
python -m pip install --upgrade pip
72+
pip install -e .
73+
pip install pytest pytest-cov pytest-asyncio websockets certifi
74+
75+
- name: Run tests
76+
run: |
77+
python -m pytest test/unit/ -v --cov=okx --cov-report=term-missing --cov-report=xml
78+
79+
- name: Upload coverage to Codecov
80+
if: matrix.python-version == '3.11'
81+
uses: codecov/codecov-action@v4
82+
with:
83+
file: ./coverage.xml
84+
fail_ci_if_error: false
85+
86+
# ============================================
87+
# BUILD JOB
88+
# ============================================
89+
build:
90+
name: Build Package
91+
runs-on: ubuntu-latest
92+
needs: [test]
93+
94+
steps:
95+
- uses: actions/checkout@v4
96+
97+
- name: Set up Python
98+
uses: actions/setup-python@v5
99+
with:
100+
python-version: "3.11"
101+
102+
- name: Install build tools
103+
run: |
104+
python -m pip install --upgrade pip
105+
pip install build twine
106+
107+
- name: Build package
108+
run: python -m build
109+
110+
- name: Check package
111+
run: twine check dist/*
112+
113+
- name: Upload build artifacts
114+
uses: actions/upload-artifact@v4
115+
with:
116+
name: dist
117+
path: dist/
118+
retention-days: 7
119+

setup.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
"loguru",
2626
"requests",
2727
"Twisted",
28-
"pyOpenSSL"
28+
"pyOpenSSL",
29+
"websockets",
30+
"certifi"
2931
]
3032
)

test/unit/okx/websocket/test_ws_private_async.py

Lines changed: 21 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,17 @@
88
import asyncio
99
from unittest.mock import patch, MagicMock, AsyncMock
1010

11+
# Import the module first so patch can resolve the path
12+
import okx.websocket.WsPrivateAsync as ws_private_module
13+
from okx.websocket.WsPrivateAsync import WsPrivateAsync
14+
1115

1216
class TestWsPrivateAsyncInit(unittest.TestCase):
1317
"""Unit tests for WsPrivateAsync initialization"""
1418

1519
def test_init_with_required_params(self):
1620
"""Test initialization with required parameters"""
17-
with patch('okx.websocket.WsPrivateAsync.WebSocketFactory') as mock_factory:
18-
from okx.websocket.WsPrivateAsync import WsPrivateAsync
21+
with patch.object(ws_private_module, 'WebSocketFactory') as mock_factory:
1922
ws = WsPrivateAsync(
2023
apiKey="test_api_key",
2124
passphrase="test_passphrase",
@@ -37,13 +40,12 @@ class TestWsPrivateAsyncSubscribe(unittest.TestCase):
3740

3841
def test_subscribe_sends_correct_payload(self):
3942
"""Test subscribe sends correct payload after login"""
40-
with patch('okx.websocket.WsPrivateAsync.WebSocketFactory'), \
41-
patch('okx.websocket.WsPrivateAsync.WsUtils.initLoginParams') as mock_init_login, \
42-
patch('okx.websocket.WsPrivateAsync.asyncio.sleep', new_callable=AsyncMock):
43+
with patch.object(ws_private_module, 'WebSocketFactory'), \
44+
patch.object(ws_private_module, 'WsUtils') as mock_ws_utils, \
45+
patch.object(ws_private_module.asyncio, 'sleep', new_callable=AsyncMock):
4346

44-
mock_init_login.return_value = '{"op":"login"}'
47+
mock_ws_utils.initLoginParams.return_value = '{"op":"login"}'
4548

46-
from okx.websocket.WsPrivateAsync import WsPrivateAsync
4749
ws = WsPrivateAsync(
4850
apiKey="test_api_key",
4951
passphrase="test_passphrase",
@@ -70,13 +72,12 @@ async def run_test():
7072

7173
def test_subscribe_with_id(self):
7274
"""Test subscribe with id parameter"""
73-
with patch('okx.websocket.WsPrivateAsync.WebSocketFactory'), \
74-
patch('okx.websocket.WsPrivateAsync.WsUtils.initLoginParams') as mock_init_login, \
75-
patch('okx.websocket.WsPrivateAsync.asyncio.sleep', new_callable=AsyncMock):
75+
with patch.object(ws_private_module, 'WebSocketFactory'), \
76+
patch.object(ws_private_module, 'WsUtils') as mock_ws_utils, \
77+
patch.object(ws_private_module.asyncio, 'sleep', new_callable=AsyncMock):
7678

77-
mock_init_login.return_value = '{"op":"login"}'
79+
mock_ws_utils.initLoginParams.return_value = '{"op":"login"}'
7880

79-
from okx.websocket.WsPrivateAsync import WsPrivateAsync
8081
ws = WsPrivateAsync(
8182
apiKey="test_api_key",
8283
passphrase="test_passphrase",
@@ -105,8 +106,7 @@ class TestWsPrivateAsyncUnsubscribe(unittest.TestCase):
105106

106107
def test_unsubscribe_sends_correct_payload(self):
107108
"""Test unsubscribe sends correct payload"""
108-
with patch('okx.websocket.WsPrivateAsync.WebSocketFactory'):
109-
from okx.websocket.WsPrivateAsync import WsPrivateAsync
109+
with patch.object(ws_private_module, 'WebSocketFactory'):
110110
ws = WsPrivateAsync(
111111
apiKey="test_api_key",
112112
passphrase="test_passphrase",
@@ -131,8 +131,7 @@ async def run_test():
131131

132132
def test_unsubscribe_with_id(self):
133133
"""Test unsubscribe with id parameter"""
134-
with patch('okx.websocket.WsPrivateAsync.WebSocketFactory'):
135-
from okx.websocket.WsPrivateAsync import WsPrivateAsync
134+
with patch.object(ws_private_module, 'WebSocketFactory'):
136135
ws = WsPrivateAsync(
137136
apiKey="test_api_key",
138137
passphrase="test_passphrase",
@@ -160,12 +159,11 @@ class TestWsPrivateAsyncLogin(unittest.TestCase):
160159

161160
def test_login_calls_init_login_params(self):
162161
"""Test login calls WsUtils.initLoginParams with correct parameters"""
163-
with patch('okx.websocket.WsPrivateAsync.WebSocketFactory'), \
164-
patch('okx.websocket.WsPrivateAsync.WsUtils.initLoginParams') as mock_init_login:
162+
with patch.object(ws_private_module, 'WebSocketFactory'), \
163+
patch.object(ws_private_module, 'WsUtils') as mock_ws_utils:
165164

166-
mock_init_login.return_value = '{"op":"login","args":[...]}'
165+
mock_ws_utils.initLoginParams.return_value = '{"op":"login","args":[...]}'
167166

168-
from okx.websocket.WsPrivateAsync import WsPrivateAsync
169167
ws = WsPrivateAsync(
170168
apiKey="test_api_key",
171169
passphrase="test_passphrase",
@@ -179,7 +177,7 @@ def test_login_calls_init_login_params(self):
179177
async def run_test():
180178
result = await ws.login()
181179
self.assertTrue(result)
182-
mock_init_login.assert_called_once_with(
180+
mock_ws_utils.initLoginParams.assert_called_once_with(
183181
useServerTime=True,
184182
apiKey="test_api_key",
185183
passphrase="test_passphrase",
@@ -193,26 +191,23 @@ class TestWsPrivateAsyncStartStop(unittest.TestCase):
193191
"""Unit tests for WsPrivateAsync start and stop methods"""
194192

195193
def test_stop(self):
196-
"""Test stop method closes the factory and stops loop"""
197-
with patch('okx.websocket.WsPrivateAsync.WebSocketFactory') as mock_factory_class:
194+
"""Test stop method closes the factory"""
195+
with patch.object(ws_private_module, 'WebSocketFactory') as mock_factory_class:
198196
mock_factory_instance = MagicMock()
199197
mock_factory_instance.close = AsyncMock()
200198
mock_factory_class.return_value = mock_factory_instance
201199

202-
from okx.websocket.WsPrivateAsync import WsPrivateAsync
203200
ws = WsPrivateAsync(
204201
apiKey="test_api_key",
205202
passphrase="test_passphrase",
206203
secretKey="test_secret_key",
207204
url="wss://test.example.com",
208205
useServerTime=False
209206
)
210-
ws.loop = MagicMock()
211207

212208
async def run_test():
213209
await ws.stop()
214210
mock_factory_instance.close.assert_called_once()
215-
ws.loop.stop.assert_called_once()
216211

217212
asyncio.get_event_loop().run_until_complete(run_test())
218213

test/unit/okx/websocket/test_ws_public_async.py

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,17 @@
88
import asyncio
99
from unittest.mock import patch, MagicMock, AsyncMock
1010

11+
# Import the module first so patch can resolve the path
12+
import okx.websocket.WsPublicAsync as ws_public_module
13+
from okx.websocket.WsPublicAsync import WsPublicAsync
14+
1115

1216
class TestWsPublicAsyncInit(unittest.TestCase):
1317
"""Unit tests for WsPublicAsync initialization"""
1418

1519
def test_init_with_url(self):
1620
"""Test initialization with url parameter"""
17-
with patch('okx.websocket.WsPublicAsync.WebSocketFactory') as mock_factory:
18-
from okx.websocket.WsPublicAsync import WsPublicAsync
21+
with patch.object(ws_public_module, 'WebSocketFactory') as mock_factory:
1922
ws = WsPublicAsync(url="wss://test.example.com")
2023

2124
self.assertEqual(ws.url, "wss://test.example.com")
@@ -29,8 +32,7 @@ class TestWsPublicAsyncSubscribe(unittest.TestCase):
2932

3033
def test_subscribe_sets_callback(self):
3134
"""Test subscribe sets callback correctly"""
32-
with patch('okx.websocket.WsPublicAsync.WebSocketFactory'):
33-
from okx.websocket.WsPublicAsync import WsPublicAsync
35+
with patch.object(ws_public_module, 'WebSocketFactory'):
3436
ws = WsPublicAsync(url="wss://test.example.com")
3537
mock_websocket = AsyncMock()
3638
ws.websocket = mock_websocket
@@ -53,8 +55,7 @@ async def run_test():
5355

5456
def test_subscribe_with_id(self):
5557
"""Test subscribe with id parameter"""
56-
with patch('okx.websocket.WsPublicAsync.WebSocketFactory'):
57-
from okx.websocket.WsPublicAsync import WsPublicAsync
58+
with patch.object(ws_public_module, 'WebSocketFactory'):
5859
ws = WsPublicAsync(url="wss://test.example.com")
5960
mock_websocket = AsyncMock()
6061
ws.websocket = mock_websocket
@@ -75,8 +76,7 @@ async def run_test():
7576

7677
def test_subscribe_with_multiple_channels(self):
7778
"""Test subscribe with multiple channels"""
78-
with patch('okx.websocket.WsPublicAsync.WebSocketFactory'):
79-
from okx.websocket.WsPublicAsync import WsPublicAsync
79+
with patch.object(ws_public_module, 'WebSocketFactory'):
8080
ws = WsPublicAsync(url="wss://test.example.com")
8181
mock_websocket = AsyncMock()
8282
ws.websocket = mock_websocket
@@ -101,8 +101,7 @@ class TestWsPublicAsyncUnsubscribe(unittest.TestCase):
101101

102102
def test_unsubscribe_without_id(self):
103103
"""Test unsubscribe without id parameter"""
104-
with patch('okx.websocket.WsPublicAsync.WebSocketFactory'):
105-
from okx.websocket.WsPublicAsync import WsPublicAsync
104+
with patch.object(ws_public_module, 'WebSocketFactory'):
106105
ws = WsPublicAsync(url="wss://test.example.com")
107106
mock_websocket = AsyncMock()
108107
ws.websocket = mock_websocket
@@ -121,8 +120,7 @@ async def run_test():
121120

122121
def test_unsubscribe_with_id(self):
123122
"""Test unsubscribe with id parameter"""
124-
with patch('okx.websocket.WsPublicAsync.WebSocketFactory'):
125-
from okx.websocket.WsPublicAsync import WsPublicAsync
123+
with patch.object(ws_public_module, 'WebSocketFactory'):
126124
ws = WsPublicAsync(url="wss://test.example.com")
127125
mock_websocket = AsyncMock()
128126
ws.websocket = mock_websocket
@@ -144,19 +142,16 @@ class TestWsPublicAsyncStartStop(unittest.TestCase):
144142

145143
def test_stop(self):
146144
"""Test stop method closes the factory"""
147-
with patch('okx.websocket.WsPublicAsync.WebSocketFactory') as mock_factory_class:
145+
with patch.object(ws_public_module, 'WebSocketFactory') as mock_factory_class:
148146
mock_factory_instance = MagicMock()
149147
mock_factory_instance.close = AsyncMock()
150148
mock_factory_class.return_value = mock_factory_instance
151149

152-
from okx.websocket.WsPublicAsync import WsPublicAsync
153150
ws = WsPublicAsync(url="wss://test.example.com")
154-
ws.loop = MagicMock()
155151

156152
async def run_test():
157153
await ws.stop()
158154
mock_factory_instance.close.assert_called_once()
159-
ws.loop.stop.assert_called_once()
160155

161156
asyncio.get_event_loop().run_until_complete(run_test())
162157

0 commit comments

Comments
 (0)