Skip to content
Open
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
4 changes: 3 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ uvicorn[standard]~=0.29.0
cryptography

DateTime~=5.5
loguru~=0.7.2
loguru~=0.7.2

python-amazon-sp-api
File renamed without changes.
64 changes: 64 additions & 0 deletions src/amazon_api/get_amazon_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
from sp_api.api import Orders
from sp_api.util import throttle_retry, load_all_pages
from sp_api.base import ApiResponse

import pprint

from constants.amazon_credentials import CREDENTIALS_ARG
from constants.status import ParserStatus
from utils.safe_ratelimit_amazon import safe_rate_limit
from utils.format_order_data import format_order_data
from schemes.shop_data import ShopData
from schemes.upload_order import OrderData
from api.parser import update_parser_status_by_id
from log.logger import logger


class OrderClient:
def __init__(self, shop: ShopData):
self.order_api = Orders(**CREDENTIALS_ARG)
self._list_orders_data = []
self.shop = shop


def _get_all_items(self, order_id) -> list:
items = []
for i_page in self._load_all_items(order_id=order_id):
for item in i_page.payload.get("OrderItems"):
items.append(item)
return items


@throttle_retry()
@load_all_pages()
def load_all_orders(self, **kwargs):
return self.order_api.get_orders(**kwargs)


@throttle_retry()
@load_all_pages()
@safe_rate_limit(header_limit=True)
def _load_all_items(self, order_id, **kwargs):
return self.order_api.get_order_items(order_id=order_id, **kwargs)


def get_orders_with_items(self, page: ApiResponse) -> list[OrderData] | None:
try:
for order in page.payload.get('Orders'):
_order_id = order["AmazonOrderId"]
logger.info(f"formating order ID: {_order_id}")
order_data = format_order_data(
order=order,
items=self._get_all_items(order_id=_order_id)
)
self._list_orders_data.append(order_data)
except Exception as e:
logger.critical(f"Some error in getting info from Amazon SP API: {e}")
pprint.pprint(e)
update_parser_status_by_id(
parser_id=self.shop.parser_id,
status=ParserStatus.OK_AND_WAIT
)
return None

return self._list_orders_data
6 changes: 4 additions & 2 deletions src/api/auth.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from configs.env import API_AUTH_TOKEN
from configs import settings
from schemes.auth import Auth


def authorization() -> Auth:
return Auth(
Authorization=f"Bearer {API_AUTH_TOKEN}"
Authorization=f"Bearer {settings.API_AUTH_TOKEN}"
)


20 changes: 13 additions & 7 deletions src/api/order.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,29 @@
import requests as req
from loguru import logger as log

from api.auth import authorization
from configs.env import API_URL
from configs import settings
from schemes.upload_order import UploadingOrderData

from log.logger import logger
from utils.retry import retry

def upload_orders_data(orders: UploadingOrderData) -> bool:

@retry()
def upload_orders_data(orders: UploadingOrderData):
logger.info("Posting data to backend...")
response = req.post(
f"{API_URL}/parser/orders/upload/",
url=settings.PARSER_ORDER_UPLOAD_URL,
headers=authorization().model_dump(),
json=orders.model_dump(),
)

if response.status_code != 200:
log.error(
logger.error(
f"""
Some error when uploading orders data, status code: {response.status_code}
"""
)
return False
return True
response.raise_for_status()



36 changes: 23 additions & 13 deletions src/api/parser.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,41 @@
import requests as req
from loguru import logger as log
from log.logger import logger

from api.auth import authorization
from configs.env import API_URL
from configs import settings


def update_parser_status_by_id(
parser_id: int, status: int, last_parsed: float | None = None
parser_id: int,
status: int,
last_parsed: float | None = None
):
data = {
"id": parser_id,
"status": status,
}

if last_parsed:
data.update({"last_parsed": last_parsed})

try:
response = req.put(
f"{API_URL}/parser/", headers=authorization().model_dump(), json=data
url=settings.PARSER_STATUS_URL,
headers=authorization().model_dump(),
json=data
)
except Exception as e:
return update_parser_status_by_id(parser_id, status)
if response.status_code != 200:
log.error(

if response.status_code != 200:
raise ValueError(f"Unexpected status code: {response.status_code}")

except ValueError:
logger.error(
f"""
Some error when updating parser status.
Parser ID: {parser_id}
Status code: {response.status_code}
Details: {response.json()['detail']}
"""
Some error when updating parser status.
Parser ID: {parser_id}
Status code: {response.status_code}
Details: {response.json()['detail']}
"""
)
except Exception as e:
logger.error(f"Some wrong with parser status updating = {e}")
19 changes: 0 additions & 19 deletions src/auth_code_endpoint.py

This file was deleted.

5 changes: 5 additions & 0 deletions src/configs/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from .env import Settings


settings = Settings() #type: ignore

32 changes: 21 additions & 11 deletions src/configs/env.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
import os
from pydantic_settings import BaseSettings, SettingsConfigDict

from dotenv import load_dotenv

load_dotenv()
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env")

ETSY_API_KEY = os.getenv('ETSY_API_KEY')
ETSY_API_SHARED_SECRET = os.getenv('ETSY_API_SHARED_SECRET')
LWA_APP_ID: str
LWA_CLIENT_SECRET: str
SP_API_REFRESH_TOKEN: str

ETSY_API_REDIRECT_URL = os.getenv('ETSY_API_REDIRECT_URL')
API_URL: str
API_AUTH_TOKEN: str

CODE_VERIFIER = os.getenv('CODE_VERIFIER')
LOG_FILE: str
DATA_FOLDER_PATH: str

API_URL = os.getenv('API_URL')
API_AUTH_TOKEN = os.getenv("API_AUTH_TOKEN")
@property
def SHOPS_DATA_FILE_PATH(self):
return f"{self.DATA_FOLDER_PATH}/shops/shops_amazon.json"

DATA_FOLDER_PATH = os.getenv('DATA_FOLDER_PATH')
LOG_FILE = os.getenv("LOG_FILE")

@property
def PARSER_ORDER_UPLOAD_URL(self):
return f"{self.API_URL}/parser/orders/upload/amazon"

@property
def PARSER_STATUS_URL(self):
return f"{self.API_URL}/parser/"
9 changes: 9 additions & 0 deletions src/constants/amazon_credentials.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from configs import settings

CREDENTIALS_ARG = dict(
refresh_token=settings.SP_API_REFRESH_TOKEN,
credentials=dict(
lwa_app_id=settings.LWA_APP_ID,
lwa_client_secret=settings.LWA_CLIENT_SECRET
)
)
15 changes: 15 additions & 0 deletions src/constants/amazon_dates.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from datetime import datetime, timezone, timedelta

# WARNING: only ISO8601, endswith == Z

#first order was offer in 2024/2/17
EARLIEST_DATE = datetime(2024, 1, 1, tzinfo=timezone.utc).isoformat().replace("+00:00", 'Z')

#current date
CURRENT_DATE=datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z')

#last month date
LAST_MONTH_DATE = (datetime.now(timezone.utc) - timedelta(30)).isoformat().replace("+00:00", "Z")

#last week date
LAST_WEEK_DATE = (datetime.now(timezone.utc) - timedelta(7)).isoformat().replace("+00:00", "Z")
4 changes: 0 additions & 4 deletions src/constants/commands.py

This file was deleted.

27 changes: 0 additions & 27 deletions src/constants/etsy_oauth.py

This file was deleted.

3 changes: 0 additions & 3 deletions src/constants/files_paths.py

This file was deleted.

5 changes: 0 additions & 5 deletions src/constants/shops_names.py

This file was deleted.

40 changes: 40 additions & 0 deletions src/constants/status.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,43 @@ class ParserStatus:
PARSING = 1
COOKIE_EXPIRED = 2
ETSY_API_ERROR = 3


class OrderStatus:
Paid = "Paid"
Canceled = "Canceled"
Completed = "Completed"
PartiallyRefunded = "Partially Refunded"
FullyRefunded = "Fully Refunded"

@classmethod
def get_backend_status(cls, amazon_status):

"""
PendingAvailability — Available only for pre-orders. The order has been placed, but the payment has not been authorized, and the release date of the product is in the future.

Pending — The order has been placed, but the payment has not yet been authorized. The order is not ready for shipment.

Unshipped — Payment is authorized, the order is ready for shipment, but no product has been shipped yet.

PartiallyShipped — One or more items have been shipped, but not all.

Shipped — All items in the order have been shipped.

InvoiceUnconfirmed — All items have been shipped, but the seller has not yet confirmed to Amazon that the invoice has been sent to the buyer.

Cancelled — The order has been cancelled.

Unfulfillable — The order cannot be completed. This status only applies to orders completed by Amazon that have not been placed on the Amazon retail website.
"""
mapping = {
"Pending": cls.Paid,
"Unshipped": cls.Paid,
"PendingAvailability": cls.Paid,
"PartiallyShipped": cls.Completed,
"Shipped": cls.Completed,
"InvoiceUnconfirmed": cls.Paid,
"Canceled": cls.Canceled,
"Unfulfillable": cls.Canceled
}
return mapping.get(amazon_status)
Loading