Skip to content

Commit

Permalink
feat: First try to fetch data (#2)
Browse files Browse the repository at this point in the history
* feat: First try to fetch data

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix code: to meet Pre-commit requirements

* fix: pre-commit guidelines, step 2

* fix: pre-commit guidelines, step 3

* fix: pre-commit errors step 4

* fix: pre-commit step 5

* fix: PR-commit 😡, try 6

* fix: pre-commit step 7, vamooooooooosss

* fix: pre-commit step 8

* fix: pre-commit step 9

* fix: pre-commit, sorting imports and froms 🥴, step 10

* fix: pre-commit formatting froms

* fix: pre-commit, como sea esto me cagontó porque la línea no era tan larga

* fix: pre-commit step 11

* fix: pre-commit step 12

* feat: adding isort to pre-commit

* me rindoooooooooooooooo

* fix: adding issort to pyproject.toml

* Fix imports

* Fix imports

* venga, a ver mezclando imports con froms

* Poetry en pre-commit

* Updated version

* try again

* poetry

* poetry

* poetry

* poetry

* Update pre-commit

* fuera ci

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* pre-commit, fighting again

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* 😭

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* the final one?

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* another try

* bug: missing DOMAIN in ConfigFlow

* Update sensors as stats.
Sum all value of the month

* Update pre-commit
Refactor APP_KEY y APP_SECRET

* Fix values

* patch: add aliexpress_api inside the project

* patch

* patch

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* include api

* include api

* remove precommit from api

* ignore pre-commit for api

* feat: use internal API handler (#4)

* feat: use internal API handler

* feat/use of Aliexpress SDK

* pre-commit fixes

* pre-commit fix

* precommit

* pc

* pc

* pre-commit fix

* pc

* pc

* pc

* pc

* pc

* pc

* pc

* pc

* pc

* pc

* pc

* pc

* pc

* pc

* pv

* 5 horas con los pre-commits

* feat: improving orders to count (#5)

* feat: improving orders to count

* feat: integrating all sensors under a device (#6)

* feat: integrating all sensors under a device

* feat: adding sensors to a devide

* feat: Adding affiliate influencer and last order sensors (#7)

* feat: integrating all sensors under a device

* feat: adding sensors to a devide

* feat: Adding affiliate influencer and last order sensors

* feat: optimize reading data (#8)

* feat: optimize reading data

* ruff fix

* feat: Translate to spanish (#9)

* feat: Translate to spanish

* feat: Translations

* Update Coordinator and reorder code

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Pre-Commit fixs

* Pre-Commit fixs

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Mguel Ángel <[email protected]>
  • Loading branch information
3 people authored Dec 25, 2024
1 parent d8cfa16 commit 5fd7fdb
Show file tree
Hide file tree
Showing 21 changed files with 1,899 additions and 875 deletions.
11 changes: 9 additions & 2 deletions .github/workflows/pylint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,15 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
cache: "poetry"

- name: 🔄 Synchronize Poetry Lock File
run: poetry lock --no-update || poetry lock

- name: 🏗 Install Python dependencies
run: poetry install --no-interaction
run: poetry install --with dev --no-interaction

- name: 🚀 Run Pylint
run: poetry run pylint custom_components/aliexpress_openplatform
run: poetry run pylint custom_components/aliexpress_openplatform --ignore=iop --disable=import-error,too-few-public-methods

- name: 🚀 Run Mypy
run: |
poetry run mypy custom_components/aliexpress_openplatform --exclude '^custom_components/aliexpress_openplatform/iop/'
4 changes: 4 additions & 0 deletions .github/workflows/ruff.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
cache: "poetry"

- name: Regenerate poetry.lock if needed
run: |
poetry lock --no-update || poetry lock
- name: 🏗 Install Python dependencies
run: poetry install --no-interaction

Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
__pycache__
.idea
/.run/HASS.run.xml
/node_modules/.cache/
custom_components/test.py
100 changes: 36 additions & 64 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,73 +1,45 @@
---
minimum_pre_commit_version: "3.0.4"
repos:
- repo: local
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.3.2
hooks:
- id: ruff-check
name: 🐶 Ruff Linter
language: system
types: [python]
entry: poetry run ruff check --fix
require_serial: true
stages: [commit, push, manual]
- id: ruff
args:
- --fix
exclude: ^(node_modules/|custom_components/aliexpress_openplatform/iop/)
- id: ruff-format
name: 🐶 Ruff Formatter
language: system
types: [python]
entry: poetry run ruff format
require_serial: true
stages: [commit, push, manual]
exclude: ^(node_modules/|custom_components/aliexpress_openplatform/iop/)
- repo: "https://github.com/pre-commit/pre-commit-hooks"
rev: "v4.5.0"
hooks:
- id: end-of-file-fixer
exclude: ^(node_modules/|custom_components/aliexpress_openplatform/iop/)
- id: trailing-whitespace
exclude: ^(node_modules/|custom_components/aliexpress_openplatform/iop/)
- id: check-json
name: { Check JSON files
language: system
types: [json]
entry: poetry run check-json
exclude: ^(node_modules/|custom_components/aliexpress_openplatform/iop/)
- id: check-toml
name: ✅ Check TOML files
language: system
types: [toml]
entry: poetry run check-toml
exclude: ^(node_modules/|custom_components/aliexpress_openplatform/iop/)
- id: check-yaml
name: ✅ Check YAML files
language: system
types: [yaml]
entry: poetry run check-yaml
- id: check-merge-conflict
name: 💥 Check for merge conflicts
language: system
types: [text]
entry: poetry run check-merge-conflict
- id: check-symlinks
name: 🔗 Check for broken symlinks
language: system
types: [symlink]
entry: poetry run check-symlinks
- id: end-of-file-fixer
name: ⮐ Fix End of Files
language: system
types: [text]
entry: poetry run end-of-file-fixer
stages: [commit, push, manual]
- id: no-commit-to-branch
name: 🛑 Don't commit to main branch
language: system
entry: poetry run no-commit-to-branch
pass_filenames: false
always_run: true
args:
- --branch=main
- id: poetry
name: 📜 Check pyproject with Poetry
language: system
entry: poetry check
pass_filenames: false
always_run: true
- id: pylint
name: 🌟 Starring code with pylint
language: system
types: [python]
entry: poetry run pylint
exclude: ^(node_modules/|custom_components/aliexpress_openplatform/iop/)
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v2.7.1
rev: v4.0.0-alpha.8
hooks:
- id: prettier
name: 🎨 Format using prettier
exclude: ^(node_modules/|custom_components/aliexpress_openplatform/iop/)
- repo: https://github.com/PyCQA/pylint
rev: "v3.0.0"
hooks:
- id: pylint
args:
- --ignore=custom_components/aliexpress_openplatform/iop
- --disable=import-error,too-few-public-methods
exclude: ^(node_modules/|custom_components/aliexpress_openplatform/iop/)
- repo: "https://github.com/pre-commit/mirrors-mypy"
rev: "v1.13.0"
hooks:
- id: "mypy"
name: "Check type hints (mypy)"
args: [--ignore-missing-imports]
verbose: true
exclude: ^(node_modules/|custom_components/aliexpress_openplatform/iop/)
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# ha-aliexpress-openplatform

A custom Home Assistant integration to track AliExpress affiliate purchases in real-time

This component enables Home Assistant users to monitor and display affiliate purchases made through AliExpress, leveraging the AliExpress Affiliate API. The integration provides dynamically updated sensors for tracking purchase details, including product names, quantities, prices, and order statuses. Users can easily visualize affiliate sales data within their Home Assistant dashboards and set up custom notifications or automations based on purchase activity.
49 changes: 38 additions & 11 deletions custom_components/aliexpress_openplatform/__init__.py
Original file line number Diff line number Diff line change
@@ -1,36 +1,63 @@
"""Aliexpress OpenPlatform."""
"""Aliexpress OpenPlatform Integration.
This module sets up the integration and handles its configuration.
"""

from __future__ import annotations

import logging
from typing import TYPE_CHECKING

from homeassistant.const import Platform

from .aliexpress_coordinator import AliexpressOpenPlatformCoordinator
from .const import DOMAIN

if TYPE_CHECKING:
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant

_LOGGER = logging.getLogger(__name__)

PLATFORMS: list[Platform] = [Platform.SENSOR]


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Initialise entry configuration."""
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(_async_update_options))
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Set up Aliexpress OpenPlatform from a config entry."""
# Check if the coordinator already exists to avoid double setup

if DOMAIN not in hass.data:
hass.data[DOMAIN] = {}

if config_entry.entry_id in hass.data[DOMAIN]:
return False

# Initialize the coordinator
coordinator = AliexpressOpenPlatformCoordinator(hass, config_entry)
hass.data[DOMAIN][config_entry.entry_id] = coordinator

# Forward the setup to the sensor platform
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
await coordinator.async_config_entry_first_refresh()

# Register the listener for option updates
config_entry.async_on_unload(
config_entry.add_update_listener(_async_update_options)
)

return True


async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Remove entry after unload component."""
return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)
"""Unload an Aliexpress OpenPlatform config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(
config_entry, PLATFORMS
)
if unload_ok:
hass.data[DOMAIN].pop(config_entry.entry_id)
return unload_ok


async def _async_update_options(hass: HomeAssistant, config_entry: ConfigEntry) -> None:
"""Handle options update."""
# update entry replacing data with new options
# Update entry replacing data with new options
hass.config_entries.async_update_entry(
config_entry, data={**config_entry.data, **config_entry.options}
)
Expand Down
146 changes: 146 additions & 0 deletions custom_components/aliexpress_openplatform/aliexpress_api_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
"""Module to handle interactions with the AliExpress API."""

from __future__ import annotations

import hashlib
import hmac
import logging
import time
from typing import Any

import requests # type: ignore[import-untyped]

from .iop import IopClient, IopRequest

# Base URL for the AliExpress API
ALIEXPRESS_API_URL = "https://api-sg.aliexpress.com/sync"
_LOGGER = logging.getLogger(__name__)


def get_order_list(
app_key: str,
app_secret: str,
query_params: dict[str, Any],
pagination: dict[str, int] | None = None,
) -> dict[str, Any]:
"""Make an API call to `aliexpress.affiliate.order.list` using the AliExpress SDK."""
client = IopClient(ALIEXPRESS_API_URL, app_key, app_secret)

request = IopRequest("aliexpress.affiliate.order.list", "POST")
for key, value in query_params.items():
request.add_api_param(key, value)
for key, value in (pagination or {}).items():
request.add_api_param(key, str(value))

response = client.execute(request)

if not response:
message = response.message if response else "Unknown error"
code = response.code if response else "Unknown code"
error_message = f"API returned an error: {message} (Code: {code})"
raise ValueError(error_message)

data = response.body
main_key = next((key for key in data if key.endswith("_response")), None)
if not main_key or "resp_result" not in data[main_key]:
error_message = f"Unexpected API response format: {data}"
raise ValueError(error_message)

return data[main_key]["resp_result"].get("result", {})


def generate_signature(secret: str, params: dict[str, Any]) -> str:
"""Generate the HMAC-SHA256 signature for API authentication.
Args:
----
secret (str): API secret key provided by AliExpress.
params (dict): Parameters to be signed.
Returns:
-------
str: HMAC-SHA256 signature in hexadecimal format.
"""
sorted_params = sorted((k, v) for k, v in params.items() if k != "sign")
concatenated_params = "".join(f"{k}{v}" for k, v in sorted_params)
return (
hmac.new(
secret.encode("utf-8"),
concatenated_params.encode("utf-8"),
hashlib.sha256,
)
.hexdigest()
.upper()
)


def get_order_list_http_request(
app_key: str,
app_secret: str,
query_params: dict[str, Any],
pagination: dict[str, int] | None = None,
) -> dict[str, Any]:
"""Make an API call to `aliexpress.affiliate.order.list` to fetch orders.
Args:
----
app_key (str): API key provided by AliExpress.
app_secret (str): API secret key provided by AliExpress.
query_params (dict): Query parameters for the API request.
pagination (dict | None): Dictionary with `page_no` and `page_size`.
Returns:
-------
dict: Processed data from the API response.
Raises:
------
ValueError: If the API response has an unexpected format.
requests.RequestException: If an HTTP request error occurs.
"""
pagination = pagination or {"page_no": 1, "page_size": 50}

# Build the query parameters
params = {
"app_key": app_key,
"timestamp": str(int(time.time() * 1000)),
"sign_method": "hmac-sha256",
"method": "aliexpress.affiliate.order.list",
"time_type": "Payment Completed Time",
**query_params,
**pagination,
}
params["sign"] = generate_signature(app_secret, params)

try:
response = requests.get(ALIEXPRESS_API_URL, params=params, timeout=10)
response.raise_for_status()
data = response.json()

# Locate the main key in the response
main_key = next((key for key in data if key.endswith("_response")), None)
if not main_key or "resp_result" not in data[main_key]:
_LOGGER.error("Unexpected API response: %s", data)
raise_format_error("The API response does not contain the expected data.")
else:
# Process results within `resp_result`
return data[main_key]["resp_result"].get("result", {})

except requests.RequestException as e:
handle_request_exception(e)
except ValueError as e:
raise_format_error(str(e))
return {}


def raise_format_error(message: str) -> None:
"""Raise a format error with a standard message."""
raise ValueError(message)


def handle_request_exception(e: requests.RequestException) -> None:
"""Raise a request error with additional context."""
error_message = f"HTTP request error to AliExpress API: {e}"
raise requests.RequestException(error_message) from e
Loading

0 comments on commit 5fd7fdb

Please sign in to comment.