From 11efc370da2f05b063082a27f39011f670c9e509 Mon Sep 17 00:00:00 2001 From: "jason.hsu" Date: Fri, 19 Dec 2025 17:04:30 +0800 Subject: [PATCH 1/5] feat: add ci cd --- .gitlab-ci.yml | 119 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 .gitlab-ci.yml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..c4f99fa --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,119 @@ +# GitLab CI/CD Configuration for python-okx +# Documentation: https://docs.gitlab.com/ee/ci/ + +# Define stages in order of execution +stages: + - lint + - test + - build + +# Global variables +variables: + PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" + +# Cache pip downloads between jobs +cache: + paths: + - .cache/pip/ + - venv/ + +# ============================================ +# LINT STAGE +# ============================================ + +lint: + stage: lint + image: python:3.11-slim + before_script: + - pip install --upgrade pip + - pip install flake8 black isort + script: + - echo "Running flake8 linting..." + - flake8 okx/ --max-line-length=120 --ignore=E501,W503,E203 --count --show-source --statistics + # Optional: Check code formatting (uncomment if you want to enforce) + # - black --check okx/ + # - isort --check-only okx/ + allow_failure: true # Set to false once codebase is cleaned up + rules: + - if: $CI_PIPELINE_SOURCE == "merge_request_event" + - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH + +# ============================================ +# TEST STAGE +# ============================================ + +# Template for test jobs +.test_template: &test_template + stage: test + before_script: + - pip install --upgrade pip + - pip install -e . + - pip install pytest pytest-cov pytest-asyncio + script: + - echo "Running unit tests..." + - python -m pytest test/unit/ -v --cov=okx --cov-report=term-missing --cov-report=xml:coverage.xml + coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/' + artifacts: + reports: + coverage_report: + coverage_format: cobertura + path: coverage.xml + paths: + - coverage.xml + expire_in: 1 week + +# Test with Python 3.9 +test:python3.9: + <<: *test_template + image: python:3.9-slim + rules: + - if: $CI_PIPELINE_SOURCE == "merge_request_event" + - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH + +# Test with Python 3.10 +test:python3.10: + <<: *test_template + image: python:3.10-slim + rules: + - if: $CI_PIPELINE_SOURCE == "merge_request_event" + - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH + +# Test with Python 3.11 +test:python3.11: + <<: *test_template + image: python:3.11-slim + rules: + - if: $CI_PIPELINE_SOURCE == "merge_request_event" + - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH + +# Test with Python 3.12 +test:python3.12: + <<: *test_template + image: python:3.12-slim + rules: + - if: $CI_PIPELINE_SOURCE == "merge_request_event" + - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH + +# ============================================ +# BUILD STAGE +# ============================================ + +build: + stage: build + image: python:3.11-slim + before_script: + - pip install --upgrade pip + - pip install build twine + script: + - echo "Building package..." + - python -m build + - echo "Checking package with twine..." + - twine check dist/* + artifacts: + paths: + - dist/ + expire_in: 1 week + rules: + - if: $CI_PIPELINE_SOURCE == "merge_request_event" + - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH + - if: $CI_COMMIT_TAG From 81c269cfd50ae6fcee6d8f1c37167b2c61484161 Mon Sep 17 00:00:00 2001 From: "jason.hsu" Date: Fri, 19 Dec 2025 17:19:14 +0800 Subject: [PATCH 2/5] feat: add ci cd and remove wrong commit for gitlab --- .github/workflows/ci.yml | 113 +++++++++++++++++++++++++++++++++++++ .gitlab-ci.yml | 119 --------------------------------------- 2 files changed, 113 insertions(+), 119 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 .gitlab-ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..117d8be --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,113 @@ +# GitHub Actions CI/CD Configuration for python-okx +name: CI + +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] + +jobs: + # ============================================ + # LINT JOB + # ============================================ + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install linting tools + run: | + python -m pip install --upgrade pip + pip install flake8 + + - name: Run flake8 + run: | + flake8 okx/ --max-line-length=120 --ignore=E501,W503,E203 --count --show-source --statistics + continue-on-error: true # Set to false once codebase is cleaned up + + # ============================================ + # TEST JOB + # ============================================ + test: + name: Test (Python ${{ matrix.python-version }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.9", "3.10", "3.11", "3.12"] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Cache pip dependencies + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('setup.py') }} + restore-keys: | + ${{ runner.os }}-pip-${{ matrix.python-version }}- + ${{ runner.os }}-pip- + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e . + pip install pytest pytest-cov pytest-asyncio + + - name: Run tests + run: | + python -m pytest test/unit/ -v --cov=okx --cov-report=term-missing --cov-report=xml + + - name: Upload coverage to Codecov + if: matrix.python-version == '3.11' + uses: codecov/codecov-action@v4 + with: + file: ./coverage.xml + fail_ci_if_error: false + + # ============================================ + # BUILD JOB + # ============================================ + build: + name: Build Package + runs-on: ubuntu-latest + needs: [test] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install build tools + run: | + python -m pip install --upgrade pip + pip install build twine + + - name: Build package + run: python -m build + + - name: Check package + run: twine check dist/* + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + retention-days: 7 + diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index c4f99fa..0000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,119 +0,0 @@ -# GitLab CI/CD Configuration for python-okx -# Documentation: https://docs.gitlab.com/ee/ci/ - -# Define stages in order of execution -stages: - - lint - - test - - build - -# Global variables -variables: - PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" - -# Cache pip downloads between jobs -cache: - paths: - - .cache/pip/ - - venv/ - -# ============================================ -# LINT STAGE -# ============================================ - -lint: - stage: lint - image: python:3.11-slim - before_script: - - pip install --upgrade pip - - pip install flake8 black isort - script: - - echo "Running flake8 linting..." - - flake8 okx/ --max-line-length=120 --ignore=E501,W503,E203 --count --show-source --statistics - # Optional: Check code formatting (uncomment if you want to enforce) - # - black --check okx/ - # - isort --check-only okx/ - allow_failure: true # Set to false once codebase is cleaned up - rules: - - if: $CI_PIPELINE_SOURCE == "merge_request_event" - - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH - -# ============================================ -# TEST STAGE -# ============================================ - -# Template for test jobs -.test_template: &test_template - stage: test - before_script: - - pip install --upgrade pip - - pip install -e . - - pip install pytest pytest-cov pytest-asyncio - script: - - echo "Running unit tests..." - - python -m pytest test/unit/ -v --cov=okx --cov-report=term-missing --cov-report=xml:coverage.xml - coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/' - artifacts: - reports: - coverage_report: - coverage_format: cobertura - path: coverage.xml - paths: - - coverage.xml - expire_in: 1 week - -# Test with Python 3.9 -test:python3.9: - <<: *test_template - image: python:3.9-slim - rules: - - if: $CI_PIPELINE_SOURCE == "merge_request_event" - - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH - -# Test with Python 3.10 -test:python3.10: - <<: *test_template - image: python:3.10-slim - rules: - - if: $CI_PIPELINE_SOURCE == "merge_request_event" - - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH - -# Test with Python 3.11 -test:python3.11: - <<: *test_template - image: python:3.11-slim - rules: - - if: $CI_PIPELINE_SOURCE == "merge_request_event" - - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH - -# Test with Python 3.12 -test:python3.12: - <<: *test_template - image: python:3.12-slim - rules: - - if: $CI_PIPELINE_SOURCE == "merge_request_event" - - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH - -# ============================================ -# BUILD STAGE -# ============================================ - -build: - stage: build - image: python:3.11-slim - before_script: - - pip install --upgrade pip - - pip install build twine - script: - - echo "Building package..." - - python -m build - - echo "Checking package with twine..." - - twine check dist/* - artifacts: - paths: - - dist/ - expire_in: 1 week - rules: - - if: $CI_PIPELINE_SOURCE == "merge_request_event" - - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH - - if: $CI_COMMIT_TAG From 89e8e25629a31761f79fbcf4ee92b7865f9d87dc Mon Sep 17 00:00:00 2001 From: "jason.hsu" Date: Fri, 19 Dec 2025 17:27:29 +0800 Subject: [PATCH 3/5] feat: add cicd --- .github/workflows/ci.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 117d8be..18ff677 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,9 +3,15 @@ name: CI on: push: - branches: [main, master] + branches: + - master + - 'release/*' + - 'releases/*' pull_request: - branches: [main, master] + branches: + - master + - 'release/*' + - 'releases/*' jobs: # ============================================ From 2d47171c08ca803740663c74d632b6281b7c0b81 Mon Sep 17 00:00:00 2001 From: "jason.hsu" Date: Fri, 19 Dec 2025 17:30:36 +0800 Subject: [PATCH 4/5] fix: missing import --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 18ff677..2e0717a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -70,7 +70,7 @@ jobs: run: | python -m pip install --upgrade pip pip install -e . - pip install pytest pytest-cov pytest-asyncio + pip install pytest pytest-cov pytest-asyncio websockets certifi - name: Run tests run: | From ee04e6b1dcecd9ef5433d04d77421a0adbbf3244 Mon Sep 17 00:00:00 2001 From: "jason.hsu" Date: Fri, 19 Dec 2025 17:41:25 +0800 Subject: [PATCH 5/5] fix: broken testing --- setup.py | 4 +- .../okx/websocket/test_ws_private_async.py | 47 +++++++++---------- .../okx/websocket/test_ws_public_async.py | 27 +++++------ 3 files changed, 35 insertions(+), 43 deletions(-) diff --git a/setup.py b/setup.py index b91e237..ce2838d 100644 --- a/setup.py +++ b/setup.py @@ -25,6 +25,8 @@ "loguru", "requests", "Twisted", - "pyOpenSSL" + "pyOpenSSL", + "websockets", + "certifi" ] ) \ No newline at end of file diff --git a/test/unit/okx/websocket/test_ws_private_async.py b/test/unit/okx/websocket/test_ws_private_async.py index 4e43114..da367f8 100644 --- a/test/unit/okx/websocket/test_ws_private_async.py +++ b/test/unit/okx/websocket/test_ws_private_async.py @@ -8,14 +8,17 @@ import asyncio from unittest.mock import patch, MagicMock, AsyncMock +# Import the module first so patch can resolve the path +import okx.websocket.WsPrivateAsync as ws_private_module +from okx.websocket.WsPrivateAsync import WsPrivateAsync + class TestWsPrivateAsyncInit(unittest.TestCase): """Unit tests for WsPrivateAsync initialization""" def test_init_with_required_params(self): """Test initialization with required parameters""" - with patch('okx.websocket.WsPrivateAsync.WebSocketFactory') as mock_factory: - from okx.websocket.WsPrivateAsync import WsPrivateAsync + with patch.object(ws_private_module, 'WebSocketFactory') as mock_factory: ws = WsPrivateAsync( apiKey="test_api_key", passphrase="test_passphrase", @@ -37,13 +40,12 @@ class TestWsPrivateAsyncSubscribe(unittest.TestCase): def test_subscribe_sends_correct_payload(self): """Test subscribe sends correct payload after login""" - with patch('okx.websocket.WsPrivateAsync.WebSocketFactory'), \ - patch('okx.websocket.WsPrivateAsync.WsUtils.initLoginParams') as mock_init_login, \ - patch('okx.websocket.WsPrivateAsync.asyncio.sleep', new_callable=AsyncMock): + with patch.object(ws_private_module, 'WebSocketFactory'), \ + patch.object(ws_private_module, 'WsUtils') as mock_ws_utils, \ + patch.object(ws_private_module.asyncio, 'sleep', new_callable=AsyncMock): - mock_init_login.return_value = '{"op":"login"}' + mock_ws_utils.initLoginParams.return_value = '{"op":"login"}' - from okx.websocket.WsPrivateAsync import WsPrivateAsync ws = WsPrivateAsync( apiKey="test_api_key", passphrase="test_passphrase", @@ -70,13 +72,12 @@ async def run_test(): def test_subscribe_with_id(self): """Test subscribe with id parameter""" - with patch('okx.websocket.WsPrivateAsync.WebSocketFactory'), \ - patch('okx.websocket.WsPrivateAsync.WsUtils.initLoginParams') as mock_init_login, \ - patch('okx.websocket.WsPrivateAsync.asyncio.sleep', new_callable=AsyncMock): + with patch.object(ws_private_module, 'WebSocketFactory'), \ + patch.object(ws_private_module, 'WsUtils') as mock_ws_utils, \ + patch.object(ws_private_module.asyncio, 'sleep', new_callable=AsyncMock): - mock_init_login.return_value = '{"op":"login"}' + mock_ws_utils.initLoginParams.return_value = '{"op":"login"}' - from okx.websocket.WsPrivateAsync import WsPrivateAsync ws = WsPrivateAsync( apiKey="test_api_key", passphrase="test_passphrase", @@ -105,8 +106,7 @@ class TestWsPrivateAsyncUnsubscribe(unittest.TestCase): def test_unsubscribe_sends_correct_payload(self): """Test unsubscribe sends correct payload""" - with patch('okx.websocket.WsPrivateAsync.WebSocketFactory'): - from okx.websocket.WsPrivateAsync import WsPrivateAsync + with patch.object(ws_private_module, 'WebSocketFactory'): ws = WsPrivateAsync( apiKey="test_api_key", passphrase="test_passphrase", @@ -131,8 +131,7 @@ async def run_test(): def test_unsubscribe_with_id(self): """Test unsubscribe with id parameter""" - with patch('okx.websocket.WsPrivateAsync.WebSocketFactory'): - from okx.websocket.WsPrivateAsync import WsPrivateAsync + with patch.object(ws_private_module, 'WebSocketFactory'): ws = WsPrivateAsync( apiKey="test_api_key", passphrase="test_passphrase", @@ -160,12 +159,11 @@ class TestWsPrivateAsyncLogin(unittest.TestCase): def test_login_calls_init_login_params(self): """Test login calls WsUtils.initLoginParams with correct parameters""" - with patch('okx.websocket.WsPrivateAsync.WebSocketFactory'), \ - patch('okx.websocket.WsPrivateAsync.WsUtils.initLoginParams') as mock_init_login: + with patch.object(ws_private_module, 'WebSocketFactory'), \ + patch.object(ws_private_module, 'WsUtils') as mock_ws_utils: - mock_init_login.return_value = '{"op":"login","args":[...]}' + mock_ws_utils.initLoginParams.return_value = '{"op":"login","args":[...]}' - from okx.websocket.WsPrivateAsync import WsPrivateAsync ws = WsPrivateAsync( apiKey="test_api_key", passphrase="test_passphrase", @@ -179,7 +177,7 @@ def test_login_calls_init_login_params(self): async def run_test(): result = await ws.login() self.assertTrue(result) - mock_init_login.assert_called_once_with( + mock_ws_utils.initLoginParams.assert_called_once_with( useServerTime=True, apiKey="test_api_key", passphrase="test_passphrase", @@ -193,13 +191,12 @@ class TestWsPrivateAsyncStartStop(unittest.TestCase): """Unit tests for WsPrivateAsync start and stop methods""" def test_stop(self): - """Test stop method closes the factory and stops loop""" - with patch('okx.websocket.WsPrivateAsync.WebSocketFactory') as mock_factory_class: + """Test stop method closes the factory""" + with patch.object(ws_private_module, 'WebSocketFactory') as mock_factory_class: mock_factory_instance = MagicMock() mock_factory_instance.close = AsyncMock() mock_factory_class.return_value = mock_factory_instance - from okx.websocket.WsPrivateAsync import WsPrivateAsync ws = WsPrivateAsync( apiKey="test_api_key", passphrase="test_passphrase", @@ -207,12 +204,10 @@ def test_stop(self): url="wss://test.example.com", useServerTime=False ) - ws.loop = MagicMock() async def run_test(): await ws.stop() mock_factory_instance.close.assert_called_once() - ws.loop.stop.assert_called_once() asyncio.get_event_loop().run_until_complete(run_test()) diff --git a/test/unit/okx/websocket/test_ws_public_async.py b/test/unit/okx/websocket/test_ws_public_async.py index 6443ac4..916a0b9 100644 --- a/test/unit/okx/websocket/test_ws_public_async.py +++ b/test/unit/okx/websocket/test_ws_public_async.py @@ -8,14 +8,17 @@ import asyncio from unittest.mock import patch, MagicMock, AsyncMock +# Import the module first so patch can resolve the path +import okx.websocket.WsPublicAsync as ws_public_module +from okx.websocket.WsPublicAsync import WsPublicAsync + class TestWsPublicAsyncInit(unittest.TestCase): """Unit tests for WsPublicAsync initialization""" def test_init_with_url(self): """Test initialization with url parameter""" - with patch('okx.websocket.WsPublicAsync.WebSocketFactory') as mock_factory: - from okx.websocket.WsPublicAsync import WsPublicAsync + with patch.object(ws_public_module, 'WebSocketFactory') as mock_factory: ws = WsPublicAsync(url="wss://test.example.com") self.assertEqual(ws.url, "wss://test.example.com") @@ -29,8 +32,7 @@ class TestWsPublicAsyncSubscribe(unittest.TestCase): def test_subscribe_sets_callback(self): """Test subscribe sets callback correctly""" - with patch('okx.websocket.WsPublicAsync.WebSocketFactory'): - from okx.websocket.WsPublicAsync import WsPublicAsync + with patch.object(ws_public_module, 'WebSocketFactory'): ws = WsPublicAsync(url="wss://test.example.com") mock_websocket = AsyncMock() ws.websocket = mock_websocket @@ -53,8 +55,7 @@ async def run_test(): def test_subscribe_with_id(self): """Test subscribe with id parameter""" - with patch('okx.websocket.WsPublicAsync.WebSocketFactory'): - from okx.websocket.WsPublicAsync import WsPublicAsync + with patch.object(ws_public_module, 'WebSocketFactory'): ws = WsPublicAsync(url="wss://test.example.com") mock_websocket = AsyncMock() ws.websocket = mock_websocket @@ -75,8 +76,7 @@ async def run_test(): def test_subscribe_with_multiple_channels(self): """Test subscribe with multiple channels""" - with patch('okx.websocket.WsPublicAsync.WebSocketFactory'): - from okx.websocket.WsPublicAsync import WsPublicAsync + with patch.object(ws_public_module, 'WebSocketFactory'): ws = WsPublicAsync(url="wss://test.example.com") mock_websocket = AsyncMock() ws.websocket = mock_websocket @@ -101,8 +101,7 @@ class TestWsPublicAsyncUnsubscribe(unittest.TestCase): def test_unsubscribe_without_id(self): """Test unsubscribe without id parameter""" - with patch('okx.websocket.WsPublicAsync.WebSocketFactory'): - from okx.websocket.WsPublicAsync import WsPublicAsync + with patch.object(ws_public_module, 'WebSocketFactory'): ws = WsPublicAsync(url="wss://test.example.com") mock_websocket = AsyncMock() ws.websocket = mock_websocket @@ -121,8 +120,7 @@ async def run_test(): def test_unsubscribe_with_id(self): """Test unsubscribe with id parameter""" - with patch('okx.websocket.WsPublicAsync.WebSocketFactory'): - from okx.websocket.WsPublicAsync import WsPublicAsync + with patch.object(ws_public_module, 'WebSocketFactory'): ws = WsPublicAsync(url="wss://test.example.com") mock_websocket = AsyncMock() ws.websocket = mock_websocket @@ -144,19 +142,16 @@ class TestWsPublicAsyncStartStop(unittest.TestCase): def test_stop(self): """Test stop method closes the factory""" - with patch('okx.websocket.WsPublicAsync.WebSocketFactory') as mock_factory_class: + with patch.object(ws_public_module, 'WebSocketFactory') as mock_factory_class: mock_factory_instance = MagicMock() mock_factory_instance.close = AsyncMock() mock_factory_class.return_value = mock_factory_instance - from okx.websocket.WsPublicAsync import WsPublicAsync ws = WsPublicAsync(url="wss://test.example.com") - ws.loop = MagicMock() async def run_test(): await ws.stop() mock_factory_instance.close.assert_called_once() - ws.loop.stop.assert_called_once() asyncio.get_event_loop().run_until_complete(run_test())