sokpy 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
sokpy-0.1.0/LICENSE ADDED
@@ -0,0 +1,8 @@
1
+
2
+ Copyright 2026 Collectseal120
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
5
+
6
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
7
+
8
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
sokpy-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,206 @@
1
+ Metadata-Version: 2.4
2
+ Name: sokpy
3
+ Version: 0.1.0
4
+ Summary: Unofficial Python wrapper for SOK / S-Kaupat APIs
5
+ Author: Collectseal120
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Collectseal120/sokpy
8
+ Project-URL: Repository, https://github.com/Collectseal120/sokpy
9
+ Requires-Python: >=3.7
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: requests
13
+ Requires-Dist: beautifulsoup4
14
+ Dynamic: license-file
15
+
16
+ # sokpy
17
+
18
+ A Python wrapper for the SOK (S-Group) API, providing easy access to store information, product data, and pricing from Finnish S-Group stores.
19
+
20
+ ## Features
21
+
22
+ - **Store Management**: Retrieve information about S-Group stores by brand or location
23
+ - **Product Information**: Get detailed product data including pricing, availability, and specifications
24
+ - **Pricing Data**: Access current prices, campaign prices, and historical pricing information
25
+ - **Category Browsing**: Browse products by categories within specific stores
26
+
27
+ ## Installation
28
+
29
+ ```bash
30
+ pip install sokpy
31
+ ```
32
+
33
+ ## Quick Start
34
+
35
+ ```python
36
+ from sokpy import SOKAPI
37
+
38
+ # Initialize the API
39
+ api = SOKAPI()
40
+
41
+ # Get a product by ID
42
+ product = api.get_product_by_id("some-product-id")
43
+ print(f"Product: {product.name}")
44
+ print(f"Price: {product.price} €")
45
+
46
+ # Get stores by brand
47
+ stores = api.get_stores_by_brand("S-Market")
48
+ for store in stores[:5]: # Show first 5 stores
49
+ print(f"Store: {store.name} - {store.location}")
50
+
51
+ # Get a specific store
52
+ store = api.get_store_by_id("some-store-id")
53
+ print(f"Store: {store.name}")
54
+ print(f"Opening hours: {store.weeklyOpeningHours}")
55
+ ```
56
+
57
+ ## API Reference
58
+
59
+ ### SOKAPI
60
+
61
+ The main API class for interacting with SOK services.
62
+
63
+ #### Methods
64
+
65
+ - `get_product_by_id(product_id: str) -> SOKProduct`: Retrieve a product by its ID
66
+ - `get_all_stores() -> dict`: Get all stores organized by brand
67
+ - `get_stores_by_brand(brand: str) -> list[SOKStore]`: Get stores for a specific brand
68
+ - `get_store_by_id(store_id: str) -> SOKStore`: Get a specific store by ID
69
+
70
+ ### SOKProduct
71
+
72
+ Represents a product with detailed information.
73
+
74
+ #### Attributes
75
+
76
+ - `product_id`: Unique product identifier
77
+ - `sokId`: SOK-specific product ID
78
+ - `name`: Product name
79
+ - `price`: Current price
80
+ - `availability`: Product availability status
81
+ - `pricing`: SOKPricing object with detailed pricing information
82
+ - `basicQuantityUnit`: Unit of measurement
83
+ - `comparisonPrice`: Price per comparison unit
84
+ - `comparisonUnit`: Unit for price comparison
85
+ - `priceUnit`: Unit for pricing
86
+ - `isAgeLimitedByAlcohol`: Whether the product is age-restricted
87
+ - `frozen`: Whether the product is frozen
88
+ - `packagingLabelCodes`: List of packaging label codes
89
+ - `brandName`: Brand name
90
+ - `packagingLabels`: List of packaging labels
91
+ - `slug`: URL slug for the product
92
+
93
+ ### SOKPricing
94
+
95
+ Contains detailed pricing information for a product.
96
+
97
+ #### Attributes
98
+
99
+ - `campaignPrice`: Current campaign price
100
+ - `lowest30DayPrice`: Lowest price in the last 30 days
101
+ - `campaignPriceValidUntil`: Campaign validity date
102
+ - `regularPrice`: Regular price
103
+ - `currentPrice`: Current price
104
+ - `salesUnit`: Unit for sales
105
+ - `comparisonPrice`: Price for comparison
106
+ - `comparisonUnit`: Unit for comparison
107
+ - `isApproximatePrice`: Whether price is approximate
108
+ - `depositPrice`: Deposit price
109
+ - `quantityMultiplier`: Quantity multiplier
110
+
111
+ ### SOKStore
112
+
113
+ Represents a store with location and product information.
114
+
115
+ #### Attributes
116
+
117
+ - `store_id`: Unique store identifier
118
+ - `slug`: URL slug
119
+ - `name`: Store name
120
+ - `brand`: Store brand (e.g., "S-Market", "Prisma")
121
+ - `domains`: List of domains
122
+ - `location`: Street address
123
+ - `postcode`: Postal code
124
+ - `postcodeName`: City name
125
+ - `weeklyOpeningHours`: Opening hours for each day
126
+ - `categories`: SOKCategories object for browsing products
127
+
128
+ #### Methods
129
+
130
+ - `get_filtered_products(slug: str, limit: int = 10) -> list[SOKProduct]`: Get products from a specific category
131
+
132
+ ## Examples
133
+
134
+ ### Finding Stores Near You
135
+
136
+ ```python
137
+ api = SOKAPI()
138
+
139
+ # Get all Prisma stores
140
+ prisma_stores = api.get_stores_by_brand("Prisma")
141
+ print(f"Found {len(prisma_stores)} Prisma stores")
142
+
143
+ # Display store information
144
+ for store in prisma_stores[:3]:
145
+ print(f"{store.name} - {store.location}, {store.postcode} {store.postcodeName}")
146
+ ```
147
+
148
+ ### Product Price Comparison
149
+
150
+ ```python
151
+ api = SOKAPI()
152
+
153
+ product = api.get_product_by_id("example-product-id")
154
+
155
+ if product.pricing:
156
+ pricing = product.pricing
157
+ print(f"Product: {product.name}")
158
+ print(f"Regular Price: {pricing.regularPrice} €")
159
+ print(f"Current Price: {pricing.currentPrice} €")
160
+ if pricing.campaignPrice:
161
+ print(f"Campaign Price: {pricing.campaignPrice} € (valid until {pricing.campaignPriceValidUntil})")
162
+ print(f"Lowest 30-day Price: {pricing.lowest30DayPrice} €")
163
+ ```
164
+
165
+ ### Browsing Store Categories
166
+
167
+ ```python
168
+ api = SOKAPI()
169
+ store = api.get_store_by_id("example-store-id")
170
+
171
+ # Get products from a specific category
172
+ dairy_products = store.get_filtered_products("maito-tuotteet", limit=20)
173
+ for product in dairy_products:
174
+ print(f"{product.name}: {product.price} €")
175
+ ```
176
+
177
+ or
178
+
179
+ ```python
180
+ api = SOKAPI()
181
+ store = api.get_store_by_id("example-store-id")
182
+
183
+ #Get products from a specific category
184
+ chewing_gums = store.categories.karkit_ja_suklaat.purukumit.products()
185
+
186
+ for product in chewing_gums:
187
+ print(f"{product.name}: {product.price} €")
188
+ ```
189
+
190
+ ## Requirements
191
+
192
+ - Python 3.7+
193
+ - requests
194
+ - beautifulsoup4
195
+
196
+ ## License
197
+
198
+ MIT
199
+
200
+ ## Contributing
201
+
202
+ Contributions are welcome! Please feel free to submit a Pull Request.
203
+
204
+ ## Disclaimer
205
+
206
+ This package is not officially affiliated with S-Group or SOK. Use responsibly and in accordance with S-Group's terms of service.
sokpy-0.1.0/README.md ADDED
@@ -0,0 +1,191 @@
1
+ # sokpy
2
+
3
+ A Python wrapper for the SOK (S-Group) API, providing easy access to store information, product data, and pricing from Finnish S-Group stores.
4
+
5
+ ## Features
6
+
7
+ - **Store Management**: Retrieve information about S-Group stores by brand or location
8
+ - **Product Information**: Get detailed product data including pricing, availability, and specifications
9
+ - **Pricing Data**: Access current prices, campaign prices, and historical pricing information
10
+ - **Category Browsing**: Browse products by categories within specific stores
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ pip install sokpy
16
+ ```
17
+
18
+ ## Quick Start
19
+
20
+ ```python
21
+ from sokpy import SOKAPI
22
+
23
+ # Initialize the API
24
+ api = SOKAPI()
25
+
26
+ # Get a product by ID
27
+ product = api.get_product_by_id("some-product-id")
28
+ print(f"Product: {product.name}")
29
+ print(f"Price: {product.price} €")
30
+
31
+ # Get stores by brand
32
+ stores = api.get_stores_by_brand("S-Market")
33
+ for store in stores[:5]: # Show first 5 stores
34
+ print(f"Store: {store.name} - {store.location}")
35
+
36
+ # Get a specific store
37
+ store = api.get_store_by_id("some-store-id")
38
+ print(f"Store: {store.name}")
39
+ print(f"Opening hours: {store.weeklyOpeningHours}")
40
+ ```
41
+
42
+ ## API Reference
43
+
44
+ ### SOKAPI
45
+
46
+ The main API class for interacting with SOK services.
47
+
48
+ #### Methods
49
+
50
+ - `get_product_by_id(product_id: str) -> SOKProduct`: Retrieve a product by its ID
51
+ - `get_all_stores() -> dict`: Get all stores organized by brand
52
+ - `get_stores_by_brand(brand: str) -> list[SOKStore]`: Get stores for a specific brand
53
+ - `get_store_by_id(store_id: str) -> SOKStore`: Get a specific store by ID
54
+
55
+ ### SOKProduct
56
+
57
+ Represents a product with detailed information.
58
+
59
+ #### Attributes
60
+
61
+ - `product_id`: Unique product identifier
62
+ - `sokId`: SOK-specific product ID
63
+ - `name`: Product name
64
+ - `price`: Current price
65
+ - `availability`: Product availability status
66
+ - `pricing`: SOKPricing object with detailed pricing information
67
+ - `basicQuantityUnit`: Unit of measurement
68
+ - `comparisonPrice`: Price per comparison unit
69
+ - `comparisonUnit`: Unit for price comparison
70
+ - `priceUnit`: Unit for pricing
71
+ - `isAgeLimitedByAlcohol`: Whether the product is age-restricted
72
+ - `frozen`: Whether the product is frozen
73
+ - `packagingLabelCodes`: List of packaging label codes
74
+ - `brandName`: Brand name
75
+ - `packagingLabels`: List of packaging labels
76
+ - `slug`: URL slug for the product
77
+
78
+ ### SOKPricing
79
+
80
+ Contains detailed pricing information for a product.
81
+
82
+ #### Attributes
83
+
84
+ - `campaignPrice`: Current campaign price
85
+ - `lowest30DayPrice`: Lowest price in the last 30 days
86
+ - `campaignPriceValidUntil`: Campaign validity date
87
+ - `regularPrice`: Regular price
88
+ - `currentPrice`: Current price
89
+ - `salesUnit`: Unit for sales
90
+ - `comparisonPrice`: Price for comparison
91
+ - `comparisonUnit`: Unit for comparison
92
+ - `isApproximatePrice`: Whether price is approximate
93
+ - `depositPrice`: Deposit price
94
+ - `quantityMultiplier`: Quantity multiplier
95
+
96
+ ### SOKStore
97
+
98
+ Represents a store with location and product information.
99
+
100
+ #### Attributes
101
+
102
+ - `store_id`: Unique store identifier
103
+ - `slug`: URL slug
104
+ - `name`: Store name
105
+ - `brand`: Store brand (e.g., "S-Market", "Prisma")
106
+ - `domains`: List of domains
107
+ - `location`: Street address
108
+ - `postcode`: Postal code
109
+ - `postcodeName`: City name
110
+ - `weeklyOpeningHours`: Opening hours for each day
111
+ - `categories`: SOKCategories object for browsing products
112
+
113
+ #### Methods
114
+
115
+ - `get_filtered_products(slug: str, limit: int = 10) -> list[SOKProduct]`: Get products from a specific category
116
+
117
+ ## Examples
118
+
119
+ ### Finding Stores Near You
120
+
121
+ ```python
122
+ api = SOKAPI()
123
+
124
+ # Get all Prisma stores
125
+ prisma_stores = api.get_stores_by_brand("Prisma")
126
+ print(f"Found {len(prisma_stores)} Prisma stores")
127
+
128
+ # Display store information
129
+ for store in prisma_stores[:3]:
130
+ print(f"{store.name} - {store.location}, {store.postcode} {store.postcodeName}")
131
+ ```
132
+
133
+ ### Product Price Comparison
134
+
135
+ ```python
136
+ api = SOKAPI()
137
+
138
+ product = api.get_product_by_id("example-product-id")
139
+
140
+ if product.pricing:
141
+ pricing = product.pricing
142
+ print(f"Product: {product.name}")
143
+ print(f"Regular Price: {pricing.regularPrice} €")
144
+ print(f"Current Price: {pricing.currentPrice} €")
145
+ if pricing.campaignPrice:
146
+ print(f"Campaign Price: {pricing.campaignPrice} € (valid until {pricing.campaignPriceValidUntil})")
147
+ print(f"Lowest 30-day Price: {pricing.lowest30DayPrice} €")
148
+ ```
149
+
150
+ ### Browsing Store Categories
151
+
152
+ ```python
153
+ api = SOKAPI()
154
+ store = api.get_store_by_id("example-store-id")
155
+
156
+ # Get products from a specific category
157
+ dairy_products = store.get_filtered_products("maito-tuotteet", limit=20)
158
+ for product in dairy_products:
159
+ print(f"{product.name}: {product.price} €")
160
+ ```
161
+
162
+ or
163
+
164
+ ```python
165
+ api = SOKAPI()
166
+ store = api.get_store_by_id("example-store-id")
167
+
168
+ #Get products from a specific category
169
+ chewing_gums = store.categories.karkit_ja_suklaat.purukumit.products()
170
+
171
+ for product in chewing_gums:
172
+ print(f"{product.name}: {product.price} €")
173
+ ```
174
+
175
+ ## Requirements
176
+
177
+ - Python 3.7+
178
+ - requests
179
+ - beautifulsoup4
180
+
181
+ ## License
182
+
183
+ MIT
184
+
185
+ ## Contributing
186
+
187
+ Contributions are welcome! Please feel free to submit a Pull Request.
188
+
189
+ ## Disclaimer
190
+
191
+ This package is not officially affiliated with S-Group or SOK. Use responsibly and in accordance with S-Group's terms of service.
@@ -0,0 +1,22 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "sokpy"
7
+ version = "0.1.0"
8
+ description = "Unofficial Python wrapper for SOK / S-Kaupat APIs"
9
+ readme = "README.md"
10
+ authors = [
11
+ {name = "Collectseal120"}
12
+ ]
13
+ license = "MIT"
14
+ requires-python = ">=3.7"
15
+ dependencies = [
16
+ "requests",
17
+ "beautifulsoup4"
18
+ ]
19
+
20
+ [project.urls]
21
+ Homepage = "https://github.com/Collectseal120/sokpy"
22
+ Repository = "https://github.com/Collectseal120/sokpy"
sokpy-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,6 @@
1
+ from .api import SOKAPI
2
+ from .pricing import SOKPricing
3
+ from .products import SOKProduct
4
+ from .stores import SOKStore
5
+
6
+ __all__ = ["SOKAPI", "SOKPricing", "SOKProduct", "SOKStore"]
@@ -0,0 +1,134 @@
1
+ import requests
2
+ import json
3
+ from . import sok_stores
4
+ from .query_hashes import STORE_SEARCH_HASH
5
+ from .stores import SOKStore
6
+ from .products import SOKProduct
7
+ from .pricing import SOKPricing
8
+ from bs4 import BeautifulSoup
9
+
10
+
11
+ class SOKAPI:
12
+ BASE_URL = "https://api.s-kaupat.fi/"
13
+
14
+ def __init__(self):
15
+ self.url = "https://api.s-kaupat.fi/"
16
+ self.session = requests.Session()
17
+ self.session.headers.update({
18
+ "User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
19
+ "Accept":"application/json",
20
+ "Referer":"https://www.s-kaupat.fi/",
21
+ "Origin":"https://www.s-kaupat.fi",
22
+ "Accept-Language":"fi-FI,fi;q=0.9,en;q=0.8"
23
+ })
24
+
25
+
26
+ def _request(self, operation_name, variables, sha256):
27
+ params = {
28
+ "operationName": operation_name,
29
+ "variables": json.dumps(variables),
30
+ "extensions": json.dumps({
31
+ "persistedQuery": {
32
+ "version": 1,
33
+ "sha256Hash": sha256
34
+ }
35
+ })
36
+ }
37
+
38
+ r = self.session.get(self.BASE_URL, params=params)
39
+ r.raise_for_status()
40
+ return r.json()
41
+
42
+ def get_product_by_id(self,id: str) -> SOKProduct:
43
+ url = f"https://www.s-kaupat.fi/tuote/moi/{id}"
44
+ response = requests.get(url)
45
+ response.raise_for_status()
46
+
47
+ soup = BeautifulSoup(response.text, "html.parser")
48
+ data = soup.find("script", {"id": "__NEXT_DATA__"})
49
+ json_data = json.loads(data.string)
50
+
51
+
52
+ p_data = {"data": v for k, v in json_data["props"]["pageProps"]["apolloState"].items() if 'Product:{"id"' in k}
53
+ p_data = p_data["data"]
54
+
55
+ pricing_data = p_data["pricing"]
56
+ pricing = None
57
+ if pricing_data != None:
58
+ pricing = SOKPricing(pricing_data["campaignPrice"],pricing_data["lowest30DayPrice"], pricing_data["campaignPriceValidUntil"],pricing_data["regularPrice"],pricing_data["currentPrice"], pricing_data["salesUnit"], pricing_data["comparisonPrice"], pricing_data["comparisonUnit"],pricing_data["isApproximatePrice"], pricing_data["depositPrice"],pricing_data["quantityMultiplier"])
59
+ product = SOKProduct(p_data["id"], p_data["sokId"],p_data["name"],p_data["price"],None,pricing, p_data["basicQuantityUnit"], p_data["comparisonPrice"], p_data["comparisonUnit"], p_data["priceUnit"], p_data["isAgeLimitedByAlcohol"], p_data["frozen"], p_data["packagingLabelCodes"], p_data["brandName"], p_data["packagingLabels"], p_data["slug"])
60
+ return product
61
+ def get_all_stores(self) -> list[SOKStore]:
62
+ stores = {}
63
+ for brand in sok_stores.ALL_STORES:
64
+ stores[brand] = self.get_stores_by_brand(brand)
65
+
66
+ return stores
67
+
68
+ def get_store_by_id(self, id: str) -> SOKStore:
69
+ url = f"https://www.s-kaupat.fi/myymala/moi/{id}"
70
+
71
+ response = requests.get(url)
72
+ response.raise_for_status()
73
+
74
+ soup = BeautifulSoup(response.content, "html.parser")
75
+ data = soup.find("script", {"id": "__NEXT_DATA__"})
76
+ json_data = json.loads(data.string)
77
+
78
+ store_data = json_data["props"]["pageProps"]["store"]
79
+ store = SOKStore(api=self,
80
+ store_id=store_data["id"],
81
+ slug=store_data["slug"],
82
+ name=store_data["name"],
83
+ brand=store_data["brand"],
84
+ domains=store_data["domains"],
85
+ location=store_data["location"]["address"]["street"]["default"],
86
+ postcode=store_data["location"]["address"]["postcode"],
87
+ postcodeName=store_data["location"]["address"]["postcodeName"]["default"],
88
+ weeklyOpeningHours=store_data["weeklyOpeningHours"]
89
+ )
90
+ return store
91
+
92
+
93
+ def get_stores_by_brand(self, brand: str) -> list[SOKStore]:
94
+ variables = {"query": None, "brand": brand, "cursor": None}
95
+
96
+ response = self._request("RemoteStoreSearch",variables, STORE_SEARCH_HASH)
97
+
98
+ data = response["data"]["searchStores"]
99
+ all_store_count = data["totalCount"]
100
+ store_count = len(data["stores"])
101
+ stores = []
102
+ for store_data in data["stores"]:
103
+ store = self.make_store(store_data)
104
+ stores.append(store)
105
+
106
+ print(f"Found {all_store_count} stores for brand {brand}")
107
+
108
+ while store_count < all_store_count:
109
+ variables["cursor"] = data["cursor"]
110
+
111
+ response = self._request("RemoteStoreSearch",variables, STORE_SEARCH_HASH)
112
+
113
+ data = response["data"]["searchStores"]
114
+ store_count += len(data["stores"])
115
+
116
+ for store_data in data["stores"]:
117
+ store = self.make_store(store_data)
118
+ stores.append(store)
119
+ return stores
120
+
121
+ def make_store(self, store_data: dict) -> SOKStore:
122
+ return SOKStore(
123
+ api=self,
124
+ store_id=store_data["id"],
125
+ slug=store_data["slug"],
126
+ name=store_data["name"],
127
+ brand=store_data["brand"],
128
+ domains=store_data["domains"],
129
+ location=store_data["location"]["address"]["street"]["default"],
130
+ postcode=store_data["location"]["address"]["postcode"],
131
+ postcodeName=store_data["location"]["address"]["postcodeName"]["default"],
132
+ weeklyOpeningHours=store_data["weeklyOpeningHours"]
133
+ )
134
+
@@ -0,0 +1,104 @@
1
+ from bs4 import BeautifulSoup
2
+ import requests
3
+ from .products import SOKProduct
4
+ from typing import List
5
+
6
+
7
+ class SOKCategory:
8
+ def __init__(self, store: "SOKStore", slug, parent=None): # type: ignore
9
+ self.store = store
10
+ self.slug = slug
11
+ self.parent = parent
12
+ self.children: dict[str, "SOKCategory"] = {}
13
+ self._loaded = False
14
+
15
+ @property
16
+ def path(self):
17
+ if self.parent:
18
+ return f"{self.parent.path}/{self.slug}"
19
+ return self.slug
20
+
21
+ def _load_children(self):
22
+ if self._loaded:
23
+ return
24
+
25
+ url = f"https://www.s-kaupat.fi/tuotteet/{self.path}"
26
+ response = requests.get(url)
27
+ soup = BeautifulSoup(response.text, "html.parser")
28
+ items = soup.find_all("div", {"class": "item-name"})
29
+
30
+ for item in items:
31
+ href = item.parent["href"]
32
+ child_slug = href.split("/")[-1]
33
+ child = SOKCategory(self.store, child_slug, parent=self)
34
+ self.add_child(child)
35
+
36
+ self._loaded = True
37
+
38
+ def add_child(self, child: "SOKCategory"):
39
+ self.children[child.slug] = child
40
+
41
+ def __getattr__(self, name: str) -> "SOKCategory":
42
+ self._load_children()
43
+ for slug, child in self.children.items():
44
+ if slug.replace("-", "_") == name:
45
+ return child
46
+ raise AttributeError(name)
47
+
48
+ def products(self, limit: int = 10) -> list["SOKProduct"]:
49
+ return self.store.get_filtered_products(self.path, limit)
50
+ def __str__(self):
51
+ return f"<SOKCategory slug='{self.slug}' path='{self.path}' children={list(self.children.keys())}>"
52
+
53
+ def __repr__(self):
54
+ return self.__str__()
55
+
56
+ def to_dict(self):
57
+ return {
58
+ "slug": self.slug,
59
+ "parent": self.parent,
60
+ "children": self.children,
61
+ }
62
+
63
+
64
+ class SOKCategories:
65
+ def __init__(self, store):
66
+ self.store = store
67
+ self.root: dict[str, SOKCategory] = {}
68
+ self._loaded = False
69
+
70
+ def _load_root_categories(self):
71
+ if self._loaded:
72
+ return
73
+
74
+ url = "https://www.s-kaupat.fi/tuotteet"
75
+ response = requests.get(url)
76
+
77
+ soup = BeautifulSoup(response.text, "html.parser")
78
+
79
+ items = soup.find_all("div", {"class": "item-name"})
80
+ for item in items:
81
+ href = item.parent["href"]
82
+ slug = href.split("/")[-1]
83
+
84
+ cat = SOKCategory(self.store, slug)
85
+ self.add_root(cat)
86
+
87
+ self._loaded = True
88
+
89
+ def add_root(self, category: "SOKCategory"):
90
+ self.root[category.slug] = category
91
+
92
+ def __getattr__(self, name: str) -> "SOKCategory":
93
+ self._load_root_categories()
94
+
95
+ for slug, cat in self.root.items():
96
+ if slug.replace("-", "_") == name:
97
+ return cat
98
+
99
+ raise AttributeError(name)
100
+ def __str__(self):
101
+ return f"<SOKCategories roots={list(self.root.keys())}>"
102
+
103
+ def __repr__(self):
104
+ return self.__str__()
@@ -0,0 +1,35 @@
1
+ from dataclasses import dataclass
2
+ import json
3
+
4
+ @dataclass
5
+ class SOKPricing():
6
+ def __init__(self, campaignPrice: float, lowest30DayPrice: float, campaignPriceValidUntil: str, regularPrice: float, currentPrice: float, salesUnit: str, comparisonPrice: float, comparisonUnit: str, isApproximatePrice: bool, depositPrice: float, quantityMultiplier: float):
7
+ self.campaignPrice = campaignPrice
8
+ self.lowest30DayPrice = lowest30DayPrice
9
+ self.campaignPriceValidUntil = campaignPriceValidUntil
10
+ self.regularPrice = regularPrice
11
+ self.currentPrice = currentPrice
12
+ self.salesUnit = salesUnit
13
+ self.comparisonPrice = comparisonPrice
14
+ self.comparisonUnit = comparisonUnit
15
+ self.isApproximatePrice = isApproximatePrice
16
+ self.depositPrice = depositPrice
17
+ self.quantityMultiplier = quantityMultiplier
18
+ def __str__(self):
19
+ return f"Campaign Price: {self.campaignPrice}, Lowest 30 Day Price: {self.lowest30DayPrice}, Campaign Price Valid Until: {self.campaignPriceValidUntil}, Regular Price: {self.regularPrice}, Current Price: {self.currentPrice}, Sales Unit: {self.salesUnit}, Comparison Price: {self.comparisonPrice}, Comparison Unit: {self.comparisonUnit}, Is Approximate Price: {self.isApproximatePrice}, Deposit Price: {self.depositPrice}, Quantity Multiplier: {self.quantityMultiplier}"
20
+ def to_dict(self):
21
+ return {
22
+ "campaignPrice": self.campaignPrice,
23
+ "lowest30DayPrice": self.lowest30DayPrice,
24
+ "campaignPriceValidUntil": self.campaignPriceValidUntil,
25
+ "regularPrice": self.regularPrice,
26
+ "currentPrice": self.currentPrice,
27
+ "salesUnit": self.salesUnit,
28
+ "comparisonPrice": self.comparisonPrice,
29
+ "comparisonUnit": self.comparisonUnit,
30
+ "isApproximatePrice": self.isApproximatePrice,
31
+ "depositPrice": self.depositPrice,
32
+ "quantityMultiplier": self.quantityMultiplier
33
+ }
34
+ def to_json(self):
35
+ return json.dumps(self.to_dict())
@@ -0,0 +1,48 @@
1
+ import json
2
+ from dataclasses import dataclass
3
+ from .pricing import SOKPricing
4
+
5
+
6
+ @dataclass
7
+ class SOKProduct:
8
+ def __init__(self, product_id: str, sokId: str, name: str, price: float, availability: str, pricing: SOKPricing, basicQuantityUnit: str, comparisonPrice: float, comparisonUnit: str, priceUnit: str, isAgeLimitedByAlcohol: bool, frozen: bool, packagingLabelCodes: list[str], brandName: str, packagingLabels: list[str], slug: str):
9
+ self.product_id = product_id
10
+ self.sokId = sokId
11
+ self.name = name
12
+ self.price = price
13
+ self.availability = availability
14
+ self.pricing = pricing
15
+ self.basicQuantityUnit = basicQuantityUnit
16
+ self.comparisonPrice = comparisonPrice
17
+ self.comparisonUnit = comparisonUnit
18
+ self.priceUnit = priceUnit
19
+ self.isAgeLimitedByAlcohol = isAgeLimitedByAlcohol
20
+ self.frozen = frozen
21
+ self.packagingLabelCodes = packagingLabelCodes
22
+ self.brandName = brandName
23
+ self.packagingLabels = packagingLabels
24
+ self.slug = slug
25
+
26
+ def __str__(self):
27
+ return f"Product ID: {self.product_id}, SOK ID: {self.sokId}, Name: {self.name}, Price: {self.price}, Availability: {self.availability}, Pricing: {self.pricing}, Basic Quantity Unit: {self.basicQuantityUnit}, Comparison Price: {self.comparisonPrice}, Comparison Unit: {self.comparisonUnit}, Price Unit: {self.priceUnit}, Is Age Limited By Alcohol: {self.isAgeLimitedByAlcohol}, Frozen: {self.frozen}, Packaging Label Codes: {self.packagingLabelCodes}, Brand Name: {self.brandName}, Packaging Labels: {self.packagingLabels}, Slug: {self.slug}"
28
+ def to_dict(self):
29
+ return {
30
+ "product_id": self.product_id,
31
+ "sokId": self.sokId,
32
+ "name": self.name,
33
+ "price": self.price,
34
+ "availability": self.availability,
35
+ "pricing": self.pricing.to_dict(),
36
+ "basicQuantityUnit": self.basicQuantityUnit,
37
+ "comparisonPrice": self.comparisonPrice,
38
+ "comparisonUnit": self.comparisonUnit,
39
+ "priceUnit": self.priceUnit,
40
+ "isAgeLimitedByAlcohol": self.isAgeLimitedByAlcohol,
41
+ "frozen": self.frozen,
42
+ "packagingLabelCodes": self.packagingLabelCodes,
43
+ "brandName": self.brandName,
44
+ "packagingLabels": self.packagingLabels,
45
+ "slug": self.slug
46
+ }
47
+ def to_json(self):
48
+ return json.dumps(self.to_dict())
@@ -0,0 +1,2 @@
1
+ STORE_SEARCH_HASH = "e49317e01c3a57b286fadd6f3ea47fd1d64adebb483943ba0e229307d15763b5"
2
+ PRODUCT_SEARCH_HASH = "908fdf2c35f4a100371ca1b429063492979006147192241341f209ee48892844"
@@ -0,0 +1,9 @@
1
+ PRISMA = "PRISMA"
2
+ S_MARKET = "S_MARKET"
3
+ FOOD_MARKET_HERKKU = "HERKKU"
4
+ ALEPA = "ALEPA"
5
+ SALE = "SALE"
6
+ SOKOS_HERKKU = "SOKOS_HERKKU"
7
+ MESTARIN_HERKKU = "MESTARIN_HERKKU"
8
+ DEFAULT_STORE_ID = "513971200"
9
+ ALL_STORES = [PRISMA, S_MARKET, FOOD_MARKET_HERKKU, ALEPA, SALE, SOKOS_HERKKU, MESTARIN_HERKKU]
@@ -0,0 +1,95 @@
1
+ import sok.categories as categories
2
+ from .query_hashes import PRODUCT_SEARCH_HASH
3
+ from dataclasses import dataclass
4
+ from .pricing import SOKPricing
5
+ from .products import SOKProduct
6
+ import json
7
+
8
+ @dataclass
9
+ class SOKStore:
10
+ def __init__(self,api: "SOKAPI", store_id: str, slug: str, name: str, brand: str, domains: list[str], location: str, postcode: str, postcodeName: str, weeklyOpeningHours: list[object]): # type: ignore
11
+ self.api = api
12
+ self.store_id = store_id
13
+ self.slug = slug
14
+ self.name = name
15
+ self.brand = brand
16
+ self.domains = domains
17
+ self.location = location
18
+ self.postcode = postcode
19
+ self.postcodeName = postcodeName
20
+ self.weeklyOpeningHours = weeklyOpeningHours
21
+ self.categories = categories.SOKCategories(self)
22
+
23
+
24
+ def get_filtered_products(self, slug: str, limit: int = 10) -> list[SOKProduct]:
25
+ variables = {
26
+ "facets":[
27
+ {"key":"brandName","order":"asc"},
28
+ {"key":"labels"}
29
+ ],
30
+ "from":0,
31
+ "limit":limit,
32
+ "queryString":"",
33
+ "slug": slug,
34
+ "storeId": self.store_id,
35
+ "fetchSponsoredContent":True,
36
+ "includeAgeLimitedByAlcohol":True,
37
+ "useRandomId":True
38
+ }
39
+
40
+ response = self.api._request("RemoteFilteredProducts", variables, PRODUCT_SEARCH_HASH)
41
+ products = []
42
+
43
+ for product_data in response["data"]["store"]["products"]["productListItems"]:
44
+ product = self.create_product(product_data["product"])
45
+ products.append(product)
46
+
47
+ all_product_count = response["data"]["store"]["products"]["total"]
48
+ product_count = len(response["data"]["store"]["products"]["productListItems"])
49
+ for i in range(limit, all_product_count, limit):
50
+ variables["from"] = i
51
+ response = self.api._request("RemoteFilteredProducts", variables, PRODUCT_SEARCH_HASH)
52
+ product_count += len(response["data"]["store"]["products"]["productListItems"])
53
+ for product_data in response["data"]["store"]["products"]["productListItems"]:
54
+ product = self.create_product(product_data["product"])
55
+ products.append(product)
56
+ print(f"Got {product_count} / {all_product_count} products for store {self.name}")
57
+ return products
58
+
59
+
60
+ def create_product(self, product_data: dict) -> SOKProduct:
61
+ pricing_data = product_data["pricing"]
62
+ pricing = SOKPricing(pricing_data["campaignPrice"], pricing_data["lowest30DayPrice"], pricing_data["campaignPriceValidUntil"], pricing_data["regularPrice"], pricing_data["currentPrice"], pricing_data["salesUnit"], pricing_data["comparisonPrice"], pricing_data["comparisonUnit"], pricing_data["isApproximatePrice"], pricing_data["depositPrice"], pricing_data["quantityMultiplier"])
63
+ return SOKProduct(
64
+ product_id=product_data["id"],
65
+ sokId=product_data["sokId"],
66
+ name=product_data["name"],
67
+ price=product_data["price"],
68
+ availability=product_data["availability"],
69
+ pricing=pricing,
70
+ basicQuantityUnit=product_data["basicQuantityUnit"],
71
+ comparisonPrice=product_data["comparisonPrice"],
72
+ comparisonUnit=product_data["comparisonUnit"],
73
+ priceUnit=product_data["priceUnit"],
74
+ isAgeLimitedByAlcohol=product_data["isAgeLimitedByAlcohol"],
75
+ frozen=product_data["frozen"],
76
+ packagingLabelCodes=product_data["packagingLabelCodes"],
77
+ brandName=product_data["brandName"],
78
+ packagingLabels=product_data["packagingLabels"],
79
+ slug=product_data["slug"]
80
+ )
81
+ def to_dict(self):
82
+ return {
83
+ "store_id": self.store_id,
84
+ "slug": self.slug,
85
+ "name": self.name,
86
+ "brand": self.brand,
87
+ "domains": self.domains,
88
+ "location": self.location,
89
+ "postcode": self.postcode,
90
+ "postcodeName": self.postcodeName,
91
+ "weeklyOpeningHours": self.weeklyOpeningHours,
92
+ "categories": {slug: cat.to_dict() for slug, cat in self.categories.root.items()} if hasattr(self, "categories") else {}
93
+ }
94
+ def to_json(self):
95
+ return json.dumps(self.to_dict(), indent=4, ensure_ascii=False)
@@ -0,0 +1,206 @@
1
+ Metadata-Version: 2.4
2
+ Name: sokpy
3
+ Version: 0.1.0
4
+ Summary: Unofficial Python wrapper for SOK / S-Kaupat APIs
5
+ Author: Collectseal120
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Collectseal120/sokpy
8
+ Project-URL: Repository, https://github.com/Collectseal120/sokpy
9
+ Requires-Python: >=3.7
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: requests
13
+ Requires-Dist: beautifulsoup4
14
+ Dynamic: license-file
15
+
16
+ # sokpy
17
+
18
+ A Python wrapper for the SOK (S-Group) API, providing easy access to store information, product data, and pricing from Finnish S-Group stores.
19
+
20
+ ## Features
21
+
22
+ - **Store Management**: Retrieve information about S-Group stores by brand or location
23
+ - **Product Information**: Get detailed product data including pricing, availability, and specifications
24
+ - **Pricing Data**: Access current prices, campaign prices, and historical pricing information
25
+ - **Category Browsing**: Browse products by categories within specific stores
26
+
27
+ ## Installation
28
+
29
+ ```bash
30
+ pip install sokpy
31
+ ```
32
+
33
+ ## Quick Start
34
+
35
+ ```python
36
+ from sokpy import SOKAPI
37
+
38
+ # Initialize the API
39
+ api = SOKAPI()
40
+
41
+ # Get a product by ID
42
+ product = api.get_product_by_id("some-product-id")
43
+ print(f"Product: {product.name}")
44
+ print(f"Price: {product.price} €")
45
+
46
+ # Get stores by brand
47
+ stores = api.get_stores_by_brand("S-Market")
48
+ for store in stores[:5]: # Show first 5 stores
49
+ print(f"Store: {store.name} - {store.location}")
50
+
51
+ # Get a specific store
52
+ store = api.get_store_by_id("some-store-id")
53
+ print(f"Store: {store.name}")
54
+ print(f"Opening hours: {store.weeklyOpeningHours}")
55
+ ```
56
+
57
+ ## API Reference
58
+
59
+ ### SOKAPI
60
+
61
+ The main API class for interacting with SOK services.
62
+
63
+ #### Methods
64
+
65
+ - `get_product_by_id(product_id: str) -> SOKProduct`: Retrieve a product by its ID
66
+ - `get_all_stores() -> dict`: Get all stores organized by brand
67
+ - `get_stores_by_brand(brand: str) -> list[SOKStore]`: Get stores for a specific brand
68
+ - `get_store_by_id(store_id: str) -> SOKStore`: Get a specific store by ID
69
+
70
+ ### SOKProduct
71
+
72
+ Represents a product with detailed information.
73
+
74
+ #### Attributes
75
+
76
+ - `product_id`: Unique product identifier
77
+ - `sokId`: SOK-specific product ID
78
+ - `name`: Product name
79
+ - `price`: Current price
80
+ - `availability`: Product availability status
81
+ - `pricing`: SOKPricing object with detailed pricing information
82
+ - `basicQuantityUnit`: Unit of measurement
83
+ - `comparisonPrice`: Price per comparison unit
84
+ - `comparisonUnit`: Unit for price comparison
85
+ - `priceUnit`: Unit for pricing
86
+ - `isAgeLimitedByAlcohol`: Whether the product is age-restricted
87
+ - `frozen`: Whether the product is frozen
88
+ - `packagingLabelCodes`: List of packaging label codes
89
+ - `brandName`: Brand name
90
+ - `packagingLabels`: List of packaging labels
91
+ - `slug`: URL slug for the product
92
+
93
+ ### SOKPricing
94
+
95
+ Contains detailed pricing information for a product.
96
+
97
+ #### Attributes
98
+
99
+ - `campaignPrice`: Current campaign price
100
+ - `lowest30DayPrice`: Lowest price in the last 30 days
101
+ - `campaignPriceValidUntil`: Campaign validity date
102
+ - `regularPrice`: Regular price
103
+ - `currentPrice`: Current price
104
+ - `salesUnit`: Unit for sales
105
+ - `comparisonPrice`: Price for comparison
106
+ - `comparisonUnit`: Unit for comparison
107
+ - `isApproximatePrice`: Whether price is approximate
108
+ - `depositPrice`: Deposit price
109
+ - `quantityMultiplier`: Quantity multiplier
110
+
111
+ ### SOKStore
112
+
113
+ Represents a store with location and product information.
114
+
115
+ #### Attributes
116
+
117
+ - `store_id`: Unique store identifier
118
+ - `slug`: URL slug
119
+ - `name`: Store name
120
+ - `brand`: Store brand (e.g., "S-Market", "Prisma")
121
+ - `domains`: List of domains
122
+ - `location`: Street address
123
+ - `postcode`: Postal code
124
+ - `postcodeName`: City name
125
+ - `weeklyOpeningHours`: Opening hours for each day
126
+ - `categories`: SOKCategories object for browsing products
127
+
128
+ #### Methods
129
+
130
+ - `get_filtered_products(slug: str, limit: int = 10) -> list[SOKProduct]`: Get products from a specific category
131
+
132
+ ## Examples
133
+
134
+ ### Finding Stores Near You
135
+
136
+ ```python
137
+ api = SOKAPI()
138
+
139
+ # Get all Prisma stores
140
+ prisma_stores = api.get_stores_by_brand("Prisma")
141
+ print(f"Found {len(prisma_stores)} Prisma stores")
142
+
143
+ # Display store information
144
+ for store in prisma_stores[:3]:
145
+ print(f"{store.name} - {store.location}, {store.postcode} {store.postcodeName}")
146
+ ```
147
+
148
+ ### Product Price Comparison
149
+
150
+ ```python
151
+ api = SOKAPI()
152
+
153
+ product = api.get_product_by_id("example-product-id")
154
+
155
+ if product.pricing:
156
+ pricing = product.pricing
157
+ print(f"Product: {product.name}")
158
+ print(f"Regular Price: {pricing.regularPrice} €")
159
+ print(f"Current Price: {pricing.currentPrice} €")
160
+ if pricing.campaignPrice:
161
+ print(f"Campaign Price: {pricing.campaignPrice} € (valid until {pricing.campaignPriceValidUntil})")
162
+ print(f"Lowest 30-day Price: {pricing.lowest30DayPrice} €")
163
+ ```
164
+
165
+ ### Browsing Store Categories
166
+
167
+ ```python
168
+ api = SOKAPI()
169
+ store = api.get_store_by_id("example-store-id")
170
+
171
+ # Get products from a specific category
172
+ dairy_products = store.get_filtered_products("maito-tuotteet", limit=20)
173
+ for product in dairy_products:
174
+ print(f"{product.name}: {product.price} €")
175
+ ```
176
+
177
+ or
178
+
179
+ ```python
180
+ api = SOKAPI()
181
+ store = api.get_store_by_id("example-store-id")
182
+
183
+ #Get products from a specific category
184
+ chewing_gums = store.categories.karkit_ja_suklaat.purukumit.products()
185
+
186
+ for product in chewing_gums:
187
+ print(f"{product.name}: {product.price} €")
188
+ ```
189
+
190
+ ## Requirements
191
+
192
+ - Python 3.7+
193
+ - requests
194
+ - beautifulsoup4
195
+
196
+ ## License
197
+
198
+ MIT
199
+
200
+ ## Contributing
201
+
202
+ Contributions are welcome! Please feel free to submit a Pull Request.
203
+
204
+ ## Disclaimer
205
+
206
+ This package is not officially affiliated with S-Group or SOK. Use responsibly and in accordance with S-Group's terms of service.
@@ -0,0 +1,16 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ sokpy/__init__.py
5
+ sokpy/api.py
6
+ sokpy/categories.py
7
+ sokpy/pricing.py
8
+ sokpy/products.py
9
+ sokpy/query_hashes.py
10
+ sokpy/sok_stores.py
11
+ sokpy/stores.py
12
+ sokpy.egg-info/PKG-INFO
13
+ sokpy.egg-info/SOURCES.txt
14
+ sokpy.egg-info/dependency_links.txt
15
+ sokpy.egg-info/requires.txt
16
+ sokpy.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ requests
2
+ beautifulsoup4
@@ -0,0 +1 @@
1
+ sokpy