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
378 changes: 354 additions & 24 deletions mercapi/mapping/definitions.py

Large diffs are not rendered by default.

28 changes: 28 additions & 0 deletions mercapi/mercapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from mercapi.mapping import map_to_class
from mercapi.models import SearchResults, Item, Profile, Items
from mercapi.models.base import ResponseModel
from mercapi.models.product import Product
from mercapi.requests import SearchRequestData
from mercapi.util import jwt

Expand Down Expand Up @@ -227,3 +228,30 @@ def _items(self, profile_id: str) -> Request:
headers=self._headers,
)
return self._sign_request(req)

async def product(self, product_id: str) -> Optional[Product]:
"""
Fetch details of a single listing published on Mercari Shops.
Use this method if Item.item_type is "ITEM_TYPE_BEYOND".

:param product_id: ID of a product
:return: all available product properties
"""
res = await self._client.send(self._product(product_id))
if res.status_code == 404:
return None

body = res.json()
return map_to_class(body, Product)

def _product(self, product_id: str) -> Request:
req = Request(
"GET",
f"https://api.mercari.jp/v1/marketplaces/shops/products/{product_id}",
params={
"view": "FULL",
"imageType": "JPEG",
},
headers=self._headers,
)
return self._sign_request(req)
1 change: 1 addition & 0 deletions mercapi/models/product/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .product import ProductDetail, Product
133 changes: 133 additions & 0 deletions mercapi/models/product/product.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
from dataclasses import dataclass
from datetime import datetime
from typing import Optional

from mercapi.models.base import ResponseModel


@dataclass
class ProductDetail(ResponseModel):
@dataclass
class ShopStats(ResponseModel):
shop_id: str
score: int
review_count: str

@property
def review_count_int(self) -> int:
return int(self.review_count)

@dataclass
class ShopItem(ResponseModel):
product_id: str
display_name: str
product_tags: list[str]
thumbnail: str
price: str

@property
def price_int(self) -> int:
return int(self.price)

@dataclass
class Shop(ResponseModel):
name: str
display_name: str
thumbnail: str
shop_stats: "ProductDetail.ShopStats"
allow_direct_message: bool
shop_items: list["ProductDetail.ShopItem"]
is_inbound_xb: bool

@dataclass
class Category(ResponseModel):
category_id: str
display_name: str
parent_id: str
root_id: str
has_child: bool

@dataclass
class Brand(ResponseModel):
brand_id: str
display_name: str

@dataclass
class Condition(ResponseModel):
display_name: str

@dataclass
class ShippingMethod(ResponseModel):
shipping_method_id: str
display_name: str
is_anonymous: bool

@dataclass
class ShippingPayer(ResponseModel):
shipping_payer_id: str
display_name: str
code: str

@dataclass
class ShippingDuration(ResponseModel):
shipping_duration_id: str
display_name: str
min_days: int
max_days: int

@dataclass
class ShippingFromArea(ResponseModel):
shipping_area_code: str
display_name: str

@dataclass
class ProductStats(ResponseModel):
product_id: str
score: int
review_count: int
likes_count: int

@dataclass
class Variant(ResponseModel):
variant_id: str
display_name: str
quantity: str
size: str

@property
def quantity_int(self) -> int:
return int(self.quantity)

shop: Shop
photos: list[str]
description: str
categories: list[Category]
brand: Brand
condition: Condition
shipping_method: ShippingMethod
shipping_payer: ShippingPayer
shipping_duration: ShippingDuration
shipping_from_area: ShippingFromArea
promotions: list[str]
product_stats: ProductStats
time_sale_details: Optional[str] # TODO find example and provide typing
variants: list[Variant]
shipping_fee_config: Optional[str] # TODO find example and provide typing
variation_grouping: Optional[str] # TODO find example and provide typing


@dataclass
class Product(ResponseModel):
name: str
display_name: str
product_tags: list[str]
thumbnail: str
price: str
create_time: datetime
update_time: datetime
attributes: list[str]
product_detail: ProductDetail

@property
def price_int(self) -> int:
return int(self.price)
10 changes: 7 additions & 3 deletions mercapi/models/search/search_result_item.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
from dataclasses import dataclass
from datetime import datetime
from typing import List, TYPE_CHECKING, Optional
from typing import List, TYPE_CHECKING, Optional, Union

from mercapi.models.product import Product

if TYPE_CHECKING:
from mercapi.models import Item, Profile
from mercapi.models.base import ResponseModel


@dataclass
class SearchResultItem(ResponseModel):

@dataclass
class Auction(ResponseModel):
id_: str
Expand All @@ -32,7 +34,9 @@ class Auction(ResponseModel):
is_no_price: bool # price==9999999 if True
auction: Auction

async def full_item(self) -> "Item":
async def full_item(self) -> Union["Item", "Product"]:
if self.item_type == "ITEM_TYPE_BEYOND":
return await self._mercapi.product(self.id_)
return await self._mercapi.item(self.id_)

async def seller(self) -> "Profile":
Expand Down
124 changes: 124 additions & 0 deletions test.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
POST https://api.mercari.jp/client_events/v1/event
Content-Type: application/json
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0
X-Platform: web
DPoP: eyJhbGciOiJFUzI1NiIsImp3ayI6eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6IkpvRnFtVjQ5RTlFdWwyZlotSC01bjdiR2VQVXZlekdCSEt4ZUtxVVlSNHc9IiwieSI6IlRRRWo4bjJwTXdjb1BfQndzTTRkSm84MzN6T3liWmFnZXNmSmFzSEYyN0E9In0sInR5cCI6ImRwb3Arand0In0.eyJpYXQiOjE2NTc0MDUzNTUsImp0aSI6IjE5NjE0YzdlLWRkOGYtYzI0Ni03MTY4LTk1NjM3NzYyMGQwNCIsImh0dSI6Imh0dHBzOi8vYXBpLm1lcmNhcmkuanAvY2xpZW50X2V2ZW50cy92MS9ldmVudCIsImh0bSI6IlBPU1QiLCJ1dWlkIjoiMWYxODcyMGUtYTRkYS1lM2JmLTM2M2ItNzVjYzJkYWVkZjkyIn0.xT2go-dep_duWgRFjhb096N8DlHUaddu52X-B4kZqpxxlzexRMVs13SYv5wACh3H-jK7x1dVg4Qa7_R2H9bh-A

{
"events": [
{
"context": {
"eventTime": "2022-07-09T:22:24:35.386Z",
"service": 3,
"deviceType": 3,
"user": {
"uuid": "1f18720e-a4da-e3bf-363b-75cc2daedf92"
}
},
"protoEvent": {
"categories": [],
"brands": [],
"conditions": [],
"impressions": [],
"id": {
"type": 11,
"location": {
"screen": "search"
}
}
}
},
{
"context": {
"eventTime": "2022-07-09T22:24:36.386Z",
"service": 3,
"deviceType": 3,
"user": {
"uuid": "1f18720e-a4da-e3bf-363b-75cc2daedf92"
}
},
"protoEvent": {
"categories": [],
"brands": [],
"conditions": [],
"impressions": [],
"id": {
"type": 10
},
"experiment": {
"id": "EDGE-2140_clarify_target_user_segments_of_mercari_lens"
}
}
},
{
"context": {
"eventTime": "2022-07-09T22:24:37.386Z",
"service": 3,
"deviceType": 3,
"user": {
"uuid": "1f18720e-a4da-e3bf-363b-75cc2daedf92"
}
},
"protoEvent": {
"categories": [],
"brands": [],
"conditions": [],
"impressions": [],
"id": {
"type": 11,
"location": {
"screen": "search"
}
}
}
}
],
"time": "2022-07-09T22:24:35.386Z",
"uid": "1657405475386"
}

###

POST https://api.mercari.jp/v2/entities:search
Content-Type: application/json
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0
DPoP: eyJhbGciOiJFUzI1NiIsImp3ayI6eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6IkN4N0JmbmdhRThFUE5PR3dIZG9VMFpjSmdmQ01BQ2VSRG9NdUwteEpYUWciLCJ5IjoiTG01SEZCQVJhLTdWb2ZURlR5aUxxODRUaEdkQXo0bGU2YWhpaTdxMXQ3WSJ9LCJ0eXAiOiJkcG9wK2p3dCJ9.eyJpYXQiOjE2NTc1Nzc3MTksImp0aSI6ImRlZGFkZmMzLTBhYjktNGRjNy02NjEwLTdkMGEwODUyMTVmNiIsImh0dSI6Imh0dHBzOi8vYXBpLm1lcmNhcmkuanAvdjIvZW50aXRpZXM6c2VhcmNoIiwiaHRtIjoiUE9TVCIsInV1aWQiOiI3MDUwNTA2ZC04ZThmLTM0YTctNmNlNS05OTFlMGU3OGMxNmUifQ.xbyvs6y1mHRghG68WNOpRae1aM2xzB1DFn093NqfTWKoUw3KBoLXsDiltx_kYCmsZ56hCxh1nFpNvzfIJi2jsA
X-Platform: web

{
"userId": "",
"pageSize": 120,
"pageToken": "",
"searchSessionId": "131cd8b54c52ca0e0428d9e698f2f658",
"indexRouting": "INDEX_ROUTING_UNSPECIFIED",
"thumbnailTypes": [],
"searchCondition": {
"keyword": "sharpnel",
"excludeKeyword": "",
"sort": "SORT_SCORE",
"order": "ORDER_DESC",
"status": [],
"sizeId": [],
"categoryId": [],
"brandId": [],
"sellerId": [],
"priceMin": 0,
"priceMax": 0,
"itemConditionId": [],
"shippingPayerId": [],
"shippingFromArea": [],
"shippingMethod": [],
"colorId": [],
"hasCoupon": false,
"attributes": [],
"itemTypes": [],
"skuIds": []
},
"defaultDatasets": [],
"serviceFrom": "suruga"
}

###

eyJhbGciOiJFUzI1NiIsImp3ayI6eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6IkpkU3JVUWNzZGpjZXIwYjdnbmUyOUtKNF9fRWUxNTZDQUVJRThpSVdIM2M9IiwieSI6Ii0xeVNpWEhLQkJJdXhaYWtPWTVwUnZETE5RRGZFejdSQzJVdmJzLVlraGc9In0sInR5cCI6ImRwb3Arand0In0.eyJpYXQiOjE2NTc1NzMyNzQsImp0aSI6IjBlM2NjMzY2LWFiOGQtN2M2My1jM2FlLTRjYjI4MTY5YjhjOSIsImh0dSI6Imh0dHBzOi8vYXBpLm1lcmNhcmkuanAvdjIvZW50aXRpZXM6c2VhcmNoIiwiaHRtIjoiUE9TVCIsInV1aWQiOiJmOTY0MzIxZi0zYzRlLWZiYjItMGQ5Zi03ZmMyYzZjNTg4NmYifQ.SRlOI-N3jbc1TzONrbNc7-5sM-GHuHQ705v_dxcQpY1o8gPy4BPdkg7fC4dQHIj_4_LWh1L4lDY9HWb0xSPOnA
eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2IiwiandrIjp7ImNydiI6IlAtMjU2Iiwia3R5IjoiRUMiLCJ4IjoiYnFxaHRSTVNVUl82a3dvdElvN1QwUm83YlQ4R1ZNYUtGVHpicFp4MnI0OCIsInkiOiJNeXAzZkNnbnFtTF9XdEp0X1hHM05tZGNzT25qRFplVUNuNnowMlEydjM4In19.eyJpYXQiOjE2NTc1NzA2MzIsImp0aSI6IjM5YWM0NDYxLTQzODYtNDU3Ny1iZmQ4LWViNTRjN2E0MDI1ZCIsImh0dSI6Imh0dHBzOi8vYXBpLm1lcmNhcmkuanAvdjIvZW50aXRpZXM6c2VhcmNoIiwiaHRtIjoiUE9TVCIsInV1aWQiOiI2OWNkZTUxOS0xODFmLTQyOGItOTc0Yy0xZDBjNjMwZmZjMGMifQ._bhBMwny-uxE2o6nXmOAUKKVpaTbCxOwLrzTC4YI1nChr2NBvmvKWE_9IF1HZeftdbCXD3D0ZRbvJJLjJ3ptQQ
Loading
Loading