Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .coverage
Binary file not shown.
79 changes: 79 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
name: CI

on:
push:
branches: [master, main]
pull_request:
branches: [master, main]

# This workflow only uses matrix variables and github.{workflow,ref} (safe
# values that cannot be controlled by untrusted users) — no untrusted
# event inputs are interpolated into run blocks.

concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
lint:
name: Lint (ruff)
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.13"
cache: pip
- run: pip install ruff
- name: ruff check
run: ruff check pyquotex tests scripts
# Format check is opt-in until the codebase is migrated to ruff
# format in a follow-up PR (would touch ~80 files in a single diff).
# Enable by running: ruff format pyquotex tests scripts

type-check:
name: Type check (mypy)
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.13"
cache: pip
- run: pip install -e ".[test]" mypy
- name: mypy (public surface only)
run: |
mypy --ignore-missing-imports --no-strict-optional \
pyquotex/types.py \
pyquotex/utils/streaming_indicators.py \
pyquotex/utils/cache.py \
pyquotex/utils/json_utils.py \
pyquotex/_api/_waits.py

test:
name: Test (py${{ matrix.python }} / ${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-24.04, macos-14]
python: ["3.12", "3.13"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
cache: pip
- run: pip install -e ".[test]"
- name: pytest (offline only)
env:
PYQUOTEX_LIVE: "0"
run: |
pytest -q --cov=pyquotex --cov-report=term-missing --cov-report=xml --junitxml=junit.xml
- name: Upload coverage XML
if: matrix.os == 'ubuntu-24.04' && matrix.python == '3.13'
uses: actions/upload-artifact@v4
with:
name: coverage-xml
path: coverage.xml
if-no-files-found: ignore
85 changes: 61 additions & 24 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,81 @@
name = "pyquotex"
version = "1.1.0"
description = "Quotex API Client written in Python."
authors = [
{ name = "cleiton", email = "cleiton.leonel@gmail.com"}]
authors = [{ name = "cleiton", email = "cleiton.leonel@gmail.com" }]
license = "MIT"
readme = "README.md"
packages = [{ include = "pyquotex" }]
requires-python = ">=3.12,<4.0"

[tool.poetry]
include = [
{ path = "pyquotex/py.typed", format = ["sdist", "wheel"] },
]

dependencies = [
"websockets (>=12.0)",
"httpx (>=0.27.0,<1.0.0)",
"pyfiglet (>=1.0.2,<2.0.0)",
"beautifulsoup4 (>=4.12.3,<5.0.0)",
"fake-useragent (==2.2.0)",
"certifi (>=2025.1.31)",
"rich (>=13.7.0,<14.0.0)",
"websockets>=12.0",
"httpx>=0.27.0,<1.0.0",
"pyfiglet>=1.0.2,<2.0.0",
"beautifulsoup4>=4.12.3,<5.0.0",
"fake-useragent==2.2.0",
"certifi>=2025.1.31",
"rich>=13.7.0,<14.0.0",
# numpy removed: the 7 calls in indicators.py have been replaced with
# pure-Python / stdlib equivalents (statistics.pstdev, list comprehensions).
# This saves ~20 MB and unblocks installation on Termux / slim images.
]

[project.optional-dependencies]
fast = ["orjson (>=3.9.0,<4.0.0)"]
fast = ["orjson>=3.9.0,<4.0.0"]
# `dev` and `test` extras are convenience for local setup; CI installs
# them explicitly via pip so changes here don't change CI behavior.
test = [
"pytest>=8.0.0",
"pytest-asyncio>=0.23.5",
"pytest-mock>=3.12.0",
"pytest-cov>=4.1.0",
]
dev = [
"pyquotex[test]",
"ruff>=0.5.0",
"mypy>=1.10.0",
]

[build-system]
requires = ["hatchling>=1.20"]
build-backend = "hatchling.build"

[tool.poetry.group.dev.dependencies]
python = ">=3.12,<4.0"
pytest = "^8.0.0"
pytest-asyncio = "^0.23.5"
pytest-mock = "^3.12.0"
[tool.hatch.build.targets.wheel]
packages = ["pyquotex"]
# py.typed marker (PEP 561) must ship in the wheel.
include = ["pyquotex/py.typed"]

[tool.hatch.build.targets.sdist]
include = [
"pyquotex",
"README.md",
"LICENSE",
]

[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]

[build-system]
requires = ["poetry-core>=2.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.ruff]
line-length = 100
target-version = "py312"
extend-exclude = [
"examples",
# Legacy network/optimization code uses idioms ruff flags but rewriting
# them is out of scope for the resilience / typing PR. Tighten over time.
"pyquotex/network/login.py",
"pyquotex/network/navigator.py",
"pyquotex/utils/optimization.py",
]

[tool.ruff.lint]
select = ["E", "F", "W", "I"]
ignore = [
"E501", # line-too-long — handled by ruff format
"E701", # one-liner `if x: return` is fine in this codebase
"E712", # `== True` comparisons in legacy ConnectionState properties
"E713", # `not (x in y)` style appears a few times in legacy code
]

[tool.ruff.lint.per-file-ignores]
"tests/*" = ["F401", "F811", "F541"] # tests intentionally import-and-discard
"scripts/*" = ["F401", "F541"]
2 changes: 1 addition & 1 deletion pyquotex/_api/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
from pyquotex import expiration
from pyquotex._api._constants import DEFAULT_TIMEOUT
from pyquotex.api import QuotexAPI
from pyquotex.config import resource_path
from pyquotex.exceptions import QuotexTimeoutError
from pyquotex.utils.account_type import AccountType
from pyquotex.utils.services import truncate
Expand All @@ -40,6 +39,7 @@ async def connect(self) -> tuple[bool, str]:
proxies=self.proxies,
on_otp_callback=self.on_otp_callback,
reconnect_policy=getattr(self, "reconnect_policy", None),
wss_url_override=getattr(self, "wss_url_override", None),
)

self.api.trace_ws = self.debug_ws_enable
Expand Down
2 changes: 1 addition & 1 deletion pyquotex/_api/history.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
from pyquotex.utils.cache import TTLCache
from pyquotex.utils.processor import (
calculate_candles,
process_candles_v2,
merge_candles,
process_candles_v2,
)

# Per-process cache of recent get_candles() responses. Keyed by
Expand Down
2 changes: 1 addition & 1 deletion pyquotex/_api/realtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
from pyquotex._api._constants import DEFAULT_TIMEOUT
from pyquotex.utils.indicators import TechnicalIndicators
from pyquotex.utils.processor import (
process_tick,
aggregate_candle,
process_tick,
)

logger = logging.getLogger(__name__)
Expand Down
34 changes: 23 additions & 11 deletions pyquotex/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,7 @@

import httpx

from .global_value import (
ConnectionState,
WebsocketStatus,
AuthStatus
)
from .global_value import AuthStatus, ConnectionState, WebsocketStatus
from .network.history import GetHistory
from .network.login import Login
from .network.logout import Logout
Expand Down Expand Up @@ -47,6 +43,7 @@ def __init__(
user_data_dir: str = ".",
on_otp_callback: Callable | None = None,
reconnect_policy: Any = None,
wss_url_override: str | None = None,
):
"""
:param str host: The hostname or ip address of a Quotex server.
Expand All @@ -56,6 +53,10 @@ def __init__(
:param proxies: The proxies of a Quotex server.
:param user_data_dir: The path browser user data dir.
:param on_otp_callback: Callback function for OTP (2FA) input.
:param wss_url_override: Replace the computed ``wss://ws2.{host}``
URL with this value. Primarily a test hook so the offline
integration tests can point at a local replay server, but
also useful for routing through a custom proxy.
"""
self.state = ConnectionState()
self.on_otp_callback = on_otp_callback
Expand Down Expand Up @@ -86,7 +87,11 @@ def __init__(

self.host = host
self.https_url = f"https://{host}"
self.wss_url = f"wss://ws2.{host}/socket.io/?EIO=3&transport=websocket"
self.wss_url = (
wss_url_override
if wss_url_override
else f"wss://ws2.{host}/socket.io/?EIO=3&transport=websocket"
)
self.wss_message: str | None = None
self.websocket_client: WebsocketClient | None = None
self._websocket_task: asyncio.Task | None = None
Expand Down Expand Up @@ -406,7 +411,7 @@ async def _on_message(self, msg: bytes | str) -> None:
if order_id:
profit = order.get("profit", 0)
win = "win" if profit > 0 else "loss"
# Check if it's in a closed list or has a
# Check if it's in a closed list or has a
# close status
is_closed = (
any(
Expand Down Expand Up @@ -546,7 +551,7 @@ async def _on_message(self, msg: bytes | str) -> None:
)
self.timesync.server_timestamp = ts # Sync server clock

# Limit realtime_price history to 1000 entries
# Limit realtime_price history to 1000 entries
# to prevent memory bloat
price_list = self.realtime_price[asset]
price_list.append({"time": ts, "price": price})
Expand Down Expand Up @@ -621,7 +626,7 @@ async def authenticate(self) -> tuple[bool, str]:
"""
Authenticates the user using the provided credentials.

Performs HTTP login, retrieves cookies and SSID token,
Performs HTTP login, retrieves cookies and SSID token,
and updates the browser session.

Returns:
Expand Down Expand Up @@ -892,11 +897,18 @@ async def start_websocket(self) -> tuple[bool, str]:
"Pragma": "no-cache",
"Sec-WebSocket-Extensions": "permessage-deflate; client_max_window_bits",
}
# Skip SSL for plain ws:// (test / proxy / dev) — the websockets
# library expects no SSLContext on a non-TLS URL.
ssl_ctx = (
self.browser._ssl_context
if self.wss_url.startswith("wss://")
else None
)
self._websocket_task = asyncio.create_task(
self.websocket_client.run_forever(
url=self.wss_url,
extra_headers=extra_headers,
ssl=self.browser._ssl_context
ssl=ssl_ctx,
)
)
for _ in range(100):
Expand Down Expand Up @@ -980,7 +992,7 @@ async def get_profile(self) -> Profile:
"""
Retrieves and parses the user profile data.

Updates the internal profile object with nickname, balances,
Updates the internal profile object with nickname, balances,
country, and timezone.

Returns:
Expand Down
1 change: 0 additions & 1 deletion pyquotex/cli/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@
cmd_sell,
)


COMMAND_REGISTRY = {
"login": cmd_login,
"balance": cmd_balance,
Expand Down
6 changes: 5 additions & 1 deletion pyquotex/cli/commands/candles.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@
from rich.console import Console
from rich.panel import Panel
from rich.progress import (
BarColumn, Progress, SpinnerColumn, TaskProgressColumn, TextColumn,
BarColumn,
Progress,
SpinnerColumn,
TaskProgressColumn,
TextColumn,
)

from pyquotex.cli.formatters import _print_candles_table, _save_candles_csv
Expand Down
4 changes: 2 additions & 2 deletions pyquotex/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def load_session(email: str, user_agent: str | None = None) -> dict[str, Any]:
"""Load session data for a specific email."""
if user_agent is None:
user_agent = UserAgent().random

output_file = Path(resource_path("session.json"))
with session_lock:
all_sessions = {}
Expand All @@ -74,7 +74,7 @@ def load_session(email: str, user_agent: str | None = None) -> dict[str, Any]:
"user_agent": user_agent
}
output_file.write_text(json.dumps(all_sessions, indent=4))

return all_sessions.get(email)


Expand Down
7 changes: 2 additions & 5 deletions pyquotex/expiration.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import calendar
import time
from datetime import (
datetime,
timedelta
)
from datetime import datetime, timedelta


def get_timestamp() -> int:
Expand Down Expand Up @@ -167,7 +164,7 @@ def get_server_timer(time_offset_seconds: int) -> int:
"""
Returns the server (UTC) timestamp based on local time and offset.

:param time_offset_seconds: The offset in seconds between local time
:param time_offset_seconds: The offset in seconds between local time
and UTC. Example: -10800 for UTC-3.
:return: An integer representing the server time as a Unix timestamp (UTC).
"""
Expand Down
Loading
Loading