Repository: Giglium/vinted_scraper (language: python)

Issue:
Use API instead of the web page results
## What is the current behavior?

Currently, we use the JSON inside the `<script type="application/json" data-component-name="<name>">` to scrap the items. As we have seen on #12, they removed them from the PDP (Product Detail Page) so they can be removed anytime.

## Describe the solution you'd like
Use the native API instead on the `<script type="application/json" data-component-name="<name>">` as we use now to get items info



Discussion / hints:

Fix patch:
diff --git a/README.md b/README.md
index 145023b..643bbba 100644
--- a/README.md
+++ b/README.md
@@ -14,58 +14,75 @@ You can install Vinted Scraper using pip:
     pip install vinted_scraper
 ```
 
-## Usage
+## Functions
+
+The package offers the following functions:
+<details>
+ <summary><code>search</code><code>(gets all the items present on the listing page)</code></summary>
 
-The package offers two functions:
+  **Parameters**
 
-1. The `search` function gets all the items present on the listing page
-2. The `get_item` function gets more information about an item, and its seller present on the item detail page.
+> | name   | type     | data type | description                                    |
+> |--------|----------|-----------|------------------------------------------------|
+> | params | optional | Dict      | Query parameters like the pagination and so on |
+</details>
 
-> If you want to parse and manage the scraped data directly you can use the `raw` functions.
+<details>
+ <summary><code>item</code><code>(gets the information about an item, and its seller present on the item detail page)</code></summary>
 
-Here's the two-way of how to use the package:
+  **Parameters**
 
-### Structured Data
+> | name   | type     | data type | description                                   |
+> |--------|----------|-----------|-----------------------------------------------|
+> | id     | required | str       | The unique identifier of the item to retrieve |
+> | params | optional | Dict      | I don't know is they exist                    |
+</details>
 
-To obtain the scraped data as a `vinted_scraper.VintedItem`, so you can:
+## Usage
+
+To obtain the scraped data as a `vinted_scraper.models.VintedItem`, so you can:
 
 ```python
-import vinted_scraper
+import vinted_scraper.VintedScraper
 
 
 def main():
+    scraper = VintedScraper("https://www.vinted.com")  # init the scraper with the baseurl
     params = {
         "search_text": "board games"
         # Add other query parameters like the pagination and so on
     }
-    items = vinted_scraper.search("https://www.vinted.com/catalog", params)  # get all the items
+    items = scraper.search(params)  # get all the items
     item = items[0]  # get the first Item of the list
-    vinted_scraper.get_item(item.url)  # get more info about a particular product
+    scraper.item(item.id)  # get more info about a particular item
 
 
 if __name__ == "__main__":
     main()
 ```
 
-> Structured Data are parsed and converted into a `vinted_scraper.VintedItem` object. If some attributes are `None` means
-> that it wasn't found in the scrap. Also, I discard some attribute that I thought was useless.
+`VintedScraper` returns structured data that are parsed and converted into a `vinted_scraper.models.VintedItem` object.
+If some attributes are `None` means that it wasn't found in the response, maybe because they are returned from other
+API.
+Also, I discard some attribute that I thought was useless but feel free to open an issue or a PR to add them.
 
-### Raw Data
+If you want to manage the JSON response directly, you should use the `VintedWrapper` object instead of `VintedScraper`.
 
-To obtain the scraped data as a `Dict`, so you can:
+Here's the way of how to use it:
 
 ```python
-import vinted_scraper
+import vinted_scraper.VintedWrapper
 
 
 def main():
+    wrapper = VintedWrapper("https://www.vinted.com")  # init the scraper with the baseurl
     params = {
         "search_text": "board games"
         # Add other query parameters like the pagination and so on
     }
-    items = vinted_scraper.raw_search("https://www.vinted.com/catalog", params)  # get all the items
-    item = items[0]  # get the first Item of the list
-    vinted_scraper.get_raw_item(item["url"])  # get more info about the item
+    items = wrapper.search(params)  # get all the items
+    item = items["items"][0]  # get the first Item of the list
+    wrapper.item(item["id"])  # get more info about a particular item
 
 
 if __name__ == "__main__":
diff --git a/build-tools b/build-tools
index aee873c..c7aefea 160000
--- a/build-tools
+++ b/build-tools
@@ -1,1 +1,1 @@
-Subproject commit aee873c68d4a972abfe4f2ca5ecb86543eed2781
+Subproject commit c7aefea006faf062191e9e3107ce1b51bc21213d
diff --git a/pyproject.toml b/pyproject.toml
index bce2c11..7d2914d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
 
 [project]
 name = "vinted_scraper"
-version = "1.1.3"
+version = "2.0.0"
 description = "A very simple Python package that scrapes the Vinted website to retrieve information about its items."
 readme = "README.md"
 requires-python = ">= 3.9"
@@ -20,8 +20,7 @@ classifiers = [
     "Intended Audience :: Education",
 ]
 dependencies = [
-    "requests>=2.31.0",
-    "beautifulsoup4>=4.12.2"
+    "requests>=2.31.0"
 ]
 
 [tool.isort]
diff --git a/requirements.txt b/requirements.txt
index 9d66445..37912b8 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1,1 @@
-requests>=2.31.0
-beautifulsoup4>=4.12.2
\ No newline at end of file
+requests>=2.31.0
\ No newline at end of file
diff --git a/src/vinted_scraper/VintedItem.py b/src/vinted_scraper/VintedItem.py
deleted file mode 100644
index fc525a3..0000000
--- a/src/vinted_scraper/VintedItem.py
+++ /dev/null
@@ -1,178 +0,0 @@
-import json
-from typing import List, Optional
-
-
-class VintedUser:
-    """
-    A class that represent a User. Note if an attribute is null this means that it wasn't found the scrap.
-    """
-
-    def __init__(self, data):
-        self.id: Optional[int] = data.get("id", None)
-        self.login: Optional[str] = data.get("login", None)
-        self.business: Optional[str] = data.get("business", None)
-        self.profile_url: Optional[str] = data.get("profile_url", None)
-        self.photo: Optional[VintedPhoto] = (
-            VintedPhoto(data["photo"]) if data.get("photo") else None
-        )
-        self.item_count: Optional[int] = data.get("item_count", None)
-        self.followers_count: Optional[int] = data.get("followers_count", None)
-        self.following_count: Optional[int] = data.get("following_count", None)
-        self.positive_feedback_count: Optional[int] = data.get(
-            "positive_feedback_count", None
-        )
-        self.neutral_feedback_count: Optional[int] = data.get(
-            "neutral_feedback_count", None
-        )
-        self.negative_feedback_count: Optional[int] = data.get(
-            "negative_feedback_count", None
-        )
-        self.feedback_count: Optional[int] = data.get("feedback_count", None)
-        self.feedback_reputation: Optional[float] = data.get(
-            "feedback_reputation", None
-        )
-        self.city: Optional[str] = data.get("city", None)
-        self.last_logged_on_ts: Optional[int] = data.get("last_logged_on_ts", None)
-        self.locale: Optional[str] = data.get("locale", None)
-
-    def __str__(self):  # pragma: no cover
-        return json.dumps(self, default=lambda o: o.__dict__, indent=1)
-
-    def __eq__(self, other):  # pragma: no cover
-        if not isinstance(other, VintedUser):
-            return False
-        return self.__dict__ == other.__dict__
-
-
-class VintedPhoto:
-    """
-    A class that represent a web image. Note if an attribute is null this means that it wasn't found the scrap.
-    """
-
-    def __init__(self, data):
-        self.id: Optional[int] = data.get("id", None)
-        self.url: Optional[str] = data.get("url", None)
-        self.image_no: Optional[int] = data.get("image_no", None)
-        self.dominant_color: Optional[str] = data.get("dominant_color", None)
-        self.dominant_color_opaque: Optional[str] = data.get(
-            "dominant_color_opaque", None
-        )
-        self.is_main: Optional[bool] = data.get("is_main", None)
-        self.is_suspicious: Optional[bool] = data.get("is_suspicious", None)
-        self.full_size_url: Optional[str] = data.get("full_size_url", None)
-
-    def __str__(self):  # pragma: no cover
-        return json.dumps(self, default=lambda o: o.__dict__)
-
-    def __eq__(self, other):  # pragma: no cover
-        if not isinstance(other, VintedPhoto):
-            return False
-        return self.__dict__ == other.__dict__
-
-
-class VintedBrand:
-    """
-    A class that represent a brand. Note if an attribute is null this means that it wasn't found the scrap.
-    """
-
-    def __init__(self, data):
-        self.title: Optional[str] = data.get("title", None)
-        self.slug: Optional[str] = data.get("slug", None)
-        self.path: Optional[str] = data.get("path", None)
-
-    def __str__(self):  # pragma: no cover
-        return json.dumps(self, default=lambda o: o.__dict__)
-
-    def __eq__(self, other):  # pragma: no cover
-        if not isinstance(other, VintedBrand):
-            return False
-        return self.__dict__ == other.__dict__
-
-
-class VintedItem:
-    """
-    A class that represent an Item. Note if an attribute is null this means that it wasn't found the scrap.
-    """
-
-    def __init__(self, data):
-        self.id: Optional[int] = data.get("id", None)
-        self.title: Optional[str] = data.get("title", None)
-        self.description: Optional[str] = data.get("description", None)
-        self.is_reserved: Optional[bool] = data.get("is_reserved", None)
-        self.is_closed: Optional[bool] = data.get("is_closed", None)
-        self.is_for_swap: Optional[bool] = data.get("is_for_swap", None)
-        self.active_bid_count: Optional[int] = data.get("active_bid_count", None)
-        self.item_closing_action: Optional[str] = data.get("item_closing_action", None)
-        self.created_at_ts: Optional[int] = data.get("created_at_ts", None)
-        self.photos: List[VintedPhoto] = [
-            VintedPhoto(p) for p in data.get("photos", [])
-        ]
-        self.thumbnail: Optional[VintedPhoto] = (
-            VintedPhoto(data["photo"]) if data.get("photo") else None
-        )
-        try:
-            self.price: Optional[float] = (
-                float(data["price"]["amount"])
-                if data.get("price") is not None
-                else None
-            )
-            self.currency: Optional[str] = (
-                data["price"]["currency_code"]
-                if data.get("price") is not None
-                else None
-            )
-            self.service_fee: Optional[float] = (
-                float(data["service_fee"]["amount"])
-                if data.get("service_fee") is not None
-                else None
-            )
-            self.total_item_price: Optional[float] = (
-                float(data["total_item_price"]["amount"])
-                if data.get("total_item_price") is not None
-                else None
-            )
-        except TypeError:
-            self.price: Optional[float] = (
-                float(data["price"]) if data.get("price") is not None else None
-            )
-            self.currency: Optional[str] = (
-                data["currency"] if data.get("currency") is not None else None
-            )
-            self.service_fee: Optional[str] = (
-                float(data["service_fee"])
-                if data.get("service_fee") is not None
-                else None
-            )
-            self.total_item_price: Optional[str] = (
-                float(data["total_item_price"])
-                if data.get("total_item_price") is not None
-                else None
-            )
-
-        self.favourite_count: Optional[int] = data.get("favourite_count", None)
-        self.is_favourite: Optional[bool] = data.get("is_favourite", None)
-        self.view_count: Optional[int] = data.get("view_count", None)
-        self.url: Optional[str] = data.get("url", None)
-        self.status: Optional[str] = data.get("status", None)
-        self.user: Optional[VintedUser] = (
-            VintedUser(data["user"]) if data.get("user") else None
-        )
-        self.brand_dto: Optional[VintedBrand] = (
-            VintedBrand(data.get("brand_dto"))
-            if data.get("brand_dto")
-            else VintedBrand({"title": data.get("brand_title")})
-            if data.get("brand_title")
-            else None
-        )
-        self.color1: Optional[str] = data.get("color1", None)
-        self.color2: Optional[str] = data.get("color2", None)
-        self.description_attributes: List[str] = data.get("description_attributes", [])
-        self.localization: Optional[str] = data.get("localization", None)
-
-    def __str__(self):  # pragma: no cover
-        return json.dumps(self, default=lambda o: o.__dict__, indent=2)
-
-    def __eq__(self, other):  # pragma: no cover
-        if not isinstance(other, VintedItem):
-            return False
-        return self.__dict__ == other.__dict__
diff --git a/src/vinted_scraper/__init__.py b/src/vinted_scraper/__init__.py
index 8fe3708..2c07500 100644
--- a/src/vinted_scraper/__init__.py
+++ b/src/vinted_scraper/__init__.py
@@ -1,115 +1,2 @@
-import json
-import os
-import random
-from typing import Dict, List, Optional
-from urllib.parse import urlparse, urlunparse
-
-import requests
-from bs4 import BeautifulSoup
-
-from .VintedItem import VintedItem
-
-cookie = "ciccio"
-
-
-def raw_search(url: str, params: Optional[Dict] = None) -> List[Dict]:
-    """
-    Retrieves items from a given search url on Vinted.
-    :param url: the Vinted url to fetch
-    :param params: an optional Dictionary with all the query parameters to append at the request.
-        Vinted support a search without any param but to perform a search you should add the `search_text` params.
-        Default value: None.
-    """
-    return list(
-        _parse_html(_curl(url, params), {"data-js-react-on-rails-store": "MainStore"})[
-            "items"
-        ]["catalogItems"]["byId"].values()
-    )
-
-
-def search(url: str, params: Optional[Dict] = None) -> list[VintedItem]:
-    """
-    Retrieves items from a given search url on Vinted.
-    :param url: the Vinted url to fetch
-    :param params: an optional Dictionary with all the query parameters to append at the request.
-        Vinted support a search without any param but to perform a search you should add the `search_text` params.
-        Default value: None.
-    """
-    return [VintedItem(i) for i in raw_search(url, params)]
-
-
-def get_raw_item(url: str) -> Dict:
-    """
-    Retrieves items from a given search url on Vinted.
-    :param url: the url Vinted url to fetch
-    """
-    parsed_url = urlparse(url)
-
-    # Remove query parameters
-    parsed_url = parsed_url._replace(query="")
-
-    # Add "/api/v2" before "/items"
-    path_parts = parsed_url.path.split("/")
-    path_parts.insert(1, "api")
-    path_parts.insert(2, "v2")
-    parsed_url = parsed_url._replace(path="/".join(path_parts))
-
-    modified_url = urlunparse(parsed_url)
-    return json.loads(_curl(modified_url))
-
-
-def get_item(url: str) -> VintedItem:
-    """
-    Retrieves items from a given search url on Vinted.
-    :param url: the url Vinted url to fetch
-    """
-    return VintedItem(get_raw_item(url))
-
-
-def _parse_html(html_content: bytes, attrs: dict[str, str]):
-    """
-    Utils function that scraps the given HTML and find `<script>` tags with the given `attrs`
-    :param html_content: html content, you can get it from requests.get() call or by "<html><!-- --></html>".encode()
-    :param attrs: an attribute to search, for example {"class": "foo"} it will search for a html like <div class="foo">
-    """
-    soup = BeautifulSoup(html_content, "html.parser")
-    script_tag = soup.find("script", attrs)
-
-    if script_tag is not None:
-        return json.loads(script_tag.string)
-    else:
-        raise RuntimeError(f"Cannot parse the content. Script tag `{attrs}` not found.")
-
-
-def _curl(url: str, params: Optional[Dict] = None) -> bytes:
-    """
-    Perform a request to Vinted and fetch its content.
-    :param url: the Vinted url to fetch
-    :param params: an optional Dictionary with all the query parameters to append at the request. Default value: None.
-    """
-    global cookie
-    headers = {
-        "User-Agent": get_random_user_agent(),
-        "Cookie": f"_vinted_fr_session={cookie}",
-    }
-    response = requests.get(url, params=params, headers=headers)
-
-    session_cookie = response.headers.get("Set-Cookie")
-    if session_cookie and "secure, _vinted_fr_session=" in session_cookie:
-        cookie = session_cookie.split("secure, _vinted_fr_session=")[1].split(";")[0]
-
-    if 200 == response.status_code:
-        return response.content
-    if 401 == response.status_code:
-        # run an empty search to get the cookie
-        raw_search("https://www.vinted.com/catalog")
-        return _curl(url, params)
-    else:
-        raise RuntimeError(f"Cannot perform search, error code: {response.status_code}")
-
-
-def get_random_user_agent():
-    with open(os.path.join(os.path.dirname(__file__), "agents.json"), "r") as file:
-        data = json.load(file)
-        random_agent = random.choice(data)
-        return random_agent["ua"]
+from .vintedScraper import VintedScraper  # noqa: F401
+from .vintedWrapper import VintedWrapper  # noqa: F401
diff --git a/src/vinted_scraper/agents.json b/src/vinted_scraper/agents.json
index 68ea66a..539de57 100644
--- a/src/vinted_scraper/agents.json
+++ b/src/vinted_scraper/agents.json
@@ -1,1 +1,1 @@
-[{"ua": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Mobile Safari/537.3", "pct": 52.28}, {"ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5.2 Mobile/15E148 Safari/604.", "pct": 10.3}, {"ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.", "pct": 4.75}, {"ua": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.3", "pct": 4.75}, {"ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.2 Mobile/15E148 Safari/604.", "pct": 4.75}, {"ua": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Mobile Safari/537.36", "pct": 1.78}, {"ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.3 Mobile/15E148 Safari/604.", "pct": 1.58}, {"ua": "Mozilla/5.0 (Linux; Android 10; moto e(6i) Build/QOH30.280-26) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.5790.138 Mobile Safari/537.3", "pct": 1.58}, {"ua": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Mobile Safari/537.3", "pct": 1.58}, {"ua": "Mozilla/5.0 (Linux; Android 13; SAMSUNG SM-A536B) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/22.0 Chrome/111.0.5563.116 Mobile Safari/537.3", "pct": 1.58}, {"ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) GSA/275.0.550423587 Mobile/15E148 Safari/604.", "pct": 1.58}, {"ua": "Mozilla/5.0 (Linux; Android 13; SAMSUNG SM-G780G) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/22.0 Chrome/111.0.5563.116 Mobile Safari/537.3", "pct": 1.58}, {"ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/113.0.5672.69 Mobile/15E148 Safari/604.", "pct": 0.79}, {"ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Mobile/15E148 Safari/604.", "pct": 0.79}, {"ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5.1 Mobile/15E148 Safari/604.", "pct": 0.79}, {"ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) GSA/235.0.482074324 Mobile/15E148 Safari/604.", "pct": 0.79}, {"ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) GSA/251.0.508228821 Mobile/15E148 Safari/604.", "pct": 0.79}, {"ua": "Mozilla/5.0 (Linux; Android 13; SAMSUNG SM-G781B) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/22.0 Chrome/111.0.5563.116 Mobile Safari/537.3", "pct": 0.79}, {"ua": "Mozilla/5.0 (Linux; Android 13; SAMSUNG SM-G990B) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/22.0 Chrome/111.0.5563.116 Mobile Safari/537.3", "pct": 0.79}, {"ua": "Mozilla/5.0 (Linux; Android 10; MAR-LX1A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.0.0 Mobile Safari/537.3", "pct": 0.79}, {"ua": "Mozilla/5.0 (Linux; Android 10; SAMSUNG SM-A750FN) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/22.0 Chrome/111.0.5563.116 Mobile Safari/537.3", "pct": 0.79}, {"ua": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Mobile Safari/537.3", "pct": 0.79}, {"ua": "Mozilla/5.0 (Linux; Android 12; SAMSUNG SM-G780G) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/22.0 Chrome/111.0.5563.116 Mobile Safari/537.3", "pct": 0.79}, {"ua": "Mozilla/5.0 (Linux; Android 12; SM-A525F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Mobile Safari/537.3", "pct": 0.79}, {"ua": "Mozilla/5.0 (Linux; Android 13; SAMSUNG SM-A715F) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/22.0 Chrome/111.0.5563.116 Mobile Safari/537.3", "pct": 0.79}, {"ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1", "pct": 0.59}, {"ua": "Mozilla/5.0 (Linux; Android 13; SAMSUNG SM-S901B) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/22.0 Chrome/111.0.5563.116 Mobile Safari/537.36", "pct": 0.4}, {"ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 15_7_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.6.6 Mobile/15E148 Safari/604.1", "pct": 0.2}, {"ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/115.0.5790.160 Mobile/15E148 Safari/604.1", "pct": 0.2}, {"ua": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36", "pct": 0.2}]
+[{"ua": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Mobile Safari/537.3", "pct": 32.38}, {"ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.", "pct": 12.53}, {"ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.", "pct": 6.27}, {"ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) GSA/283.0.567502095 Mobile/15E148 Safari/604.", "pct": 4.18}, {"ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/117.0.5938.117 Mobile/15E148 Safari/604.", "pct": 4.18}, {"ua": "Mozilla/5.0 (Linux; Android 13; SAMSUNG SM-S911B) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/22.0 Chrome/111.0.5563.116 Mobile Safari/537.3", "pct": 3.13}, {"ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5.1 Mobile/15E148 Safari/604.", "pct": 3.13}, {"ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/117.0.5938.108 Mobile/15E148 Safari/604.", "pct": 3.13}, {"ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/117.0.5938.117 Mobile/15E148 Safari/604.", "pct": 3.13}, {"ua": "Mozilla/5.0 (Linux; Android 13; SAMSUNG SM-A145R) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/22.0 Chrome/111.0.5563.116 Mobile Safari/537.3", "pct": 2.09}, {"ua": "Mozilla/5.0 (Linux; Android 10; MAR-LX1A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.0.0 Mobile Safari/537.3", "pct": 2.09}, {"ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.2 Mobile/15E148 Safari/604.", "pct": 2.09}, {"ua": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Mobile Safari/537.3", "pct": 1.04}, {"ua": "Mozilla/5.0 (Linux; Android 13; SM-S901B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.75 Mobile Safari/537.36 ABB/3.0.", "pct": 1.04}, {"ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.", "pct": 1.04}, {"ua": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.3", "pct": 1.04}, {"ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.", "pct": 1.04}, {"ua": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Mobile Safari/537.3", "pct": 1.04}, {"ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_1_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.1 Mobile/15E148 Safari/604.", "pct": 1.04}, {"ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 15_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.6.1 Mobile/15E148 Safari/604.", "pct": 1.04}, {"ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_8_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.2 Mobile/15E148 Safari/604.", "pct": 1.04}, {"ua": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Mobile Safari/537.3", "pct": 1.04}, {"ua": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Mobile Safari/537.36", "pct": 1.04}, {"ua": "Mozilla/5.0 (Linux; Android 12; SAMSUNG SM-G991B) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/22.0 Chrome/111.0.5563.116 Mobile Safari/537.3", "pct": 1.04}, {"ua": "Mozilla/5.0 (Linux; Android 13; SAMSUNG SM-A137F) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/22.0 Chrome/111.0.5563.116 Mobile Safari/537.3", "pct": 1.04}, {"ua": "Mozilla/5.0 (Linux; Android 13; SAMSUNG SM-A226B) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/22.0 Chrome/111.0.5563.116 Mobile Safari/537.3", "pct": 1.04}, {"ua": "Mozilla/5.0 (Linux; Android 13; SAMSUNG SM-A336B) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/22.0 Chrome/111.0.5563.116 Mobile Safari/537.3", "pct": 1.04}, {"ua": "Mozilla/5.0 (Linux; Android 13; SAMSUNG SM-A515F) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/22.0 Chrome/111.0.5563.116 Mobile Safari/537.3", "pct": 1.04}, {"ua": "Mozilla/5.0 (Linux; Android 13; SAMSUNG SM-A546B) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/22.0 Chrome/111.0.5563.116 Mobile Safari/537.3", "pct": 1.04}, {"ua": "Mozilla/5.0 (Linux; Android 13; SAMSUNG SM-G980F) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/22.0 Chrome/111.0.5563.116 Mobile Safari/537.3", "pct": 1.04}, {"ua": "Mozilla/5.0 (Linux; Android 9; SAMSUNG SM-G390F) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/22.0 Chrome/111.0.5563.116 Mobile Safari/537.3", "pct": 1.04}, {"ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1", "pct": 0.52}, {"ua": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Mobile Safari/537.36", "pct": 0.52}, {"ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 YJApp-IOS jp.co.yahoo.ipn.appli/4.97.5", "pct": 0.26}, {"ua": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36", "pct": 0.26}, {"ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1", "pct": 0.26}]
diff --git a/src/vinted_scraper/models/__init__.py b/src/vinted_scraper/models/__init__.py
new file mode 100644
index 0000000..f6c6aeb
--- /dev/null
+++ b/src/vinted_scraper/models/__init__.py
@@ -0,0 +1,9 @@
+from .vintedBrand import VintedBrand  # noqa: F401
+from .vintedBundleDiscount import VintedBundleDiscount  # noqa: F401
+from .vintedBundleDiscount import VintedDiscount  # noqa: F401
+from .vintedHighResolution import VintedHighResolution  # noqa: F401
+from .vintedImage import VintedImage  # noqa: F401
+from .vintedItem import VintedItem  # noqa: F401
+from .vintedMedia import VintedMedia  # noqa: F401
+from .vintedPaymentMethod import VintedPaymentMethod  # noqa: F401
+from .vintedUser import VintedUser  # noqa: F401
diff --git a/src/vinted_scraper/models/vintedBrand.py b/src/vinted_scraper/models/vintedBrand.py
new file mode 100644
index 0000000..7321898
--- /dev/null
+++ b/src/vinted_scraper/models/vintedBrand.py
@@ -0,0 +1,23 @@
+from dataclasses import dataclass
+from typing import Optional
+
+
+@dataclass
+class VintedBrand:
+    id: Optional[str] = None
+    title: Optional[str] = None
+    slug: Optional[str] = None
+    favourite_count: Optional[int] = None
+    pretty_favourite_count: Optional[str] = None
+    item_count: Optional[int] = None
+    pretty_item_count: Optional[str] = None
+    is_visible_in_listings: Optional[bool] = None
+    requires_authenticity_check: Optional[bool] = None
+    is_luxury: Optional[bool] = None
+    path: Optional[str] = None
+    url: Optional[str] = None
+    is_favourite: Optional[bool] = None
+
+    def __init__(self, json_data=None):
+        if json_data is not None:
+            self.__dict__.update(json_data)
diff --git a/src/vinted_scraper/models/vintedBundleDiscount.py b/src/vinted_scraper/models/vintedBundleDiscount.py
new file mode 100644
index 0000000..422e72b
--- /dev/null
+++ b/src/vinted_scraper/models/vintedBundleDiscount.py
@@ -0,0 +1,31 @@
+from dataclasses import dataclass
+from typing import List, Optional
+
+
+@dataclass
+class VintedDiscount:
+    minimal_item_count: Optional[int] = None
+    fraction: Optional[float] = None
+
+    def __init__(self, json_data=None):
+        if json_data is not None:
+            self.__dict__.update(json_data)
+
+
+@dataclass
+class VintedBundleDiscount:
+    id: Optional[str] = None
+    user_id: Optional[str] = None
+    enabled: Optional[bool] = None
+    minimal_item_count: Optional[int] = None
+    fraction: Optional[float] = None
+    discounts: Optional[List[VintedDiscount]] = None
+
+    def __init__(self, json_data=None):
+        if json_data is not None:
+            self.__dict__.update(json_data)
+
+            if "discounts" in json_data:
+                self.discounts = [
+                    VintedDiscount(discount) for discount in json_data.get("discounts")
+                ]
diff --git a/src/vinted_scraper/models/vintedHighResolution.py b/src/vinted_scraper/models/vintedHighResolution.py
new file mode 100644
index 0000000..8cae9db
--- /dev/null
+++ b/src/vinted_scraper/models/vintedHighResolution.py
@@ -0,0 +1,13 @@
+from dataclasses import dataclass
+from typing import Optional
+
+
+@dataclass
+class VintedHighResolution:
+    id: Optional[str] = None
+    timestamp: Optional[int] = None
+    orientation: Optional[str] = None
+
+    def __init__(self, json_data=None):
+        if json_data is not None:
+            self.__dict__.update(json_data)
diff --git a/src/vinted_scraper/models/vintedImage.py b/src/vinted_scraper/models/vintedImage.py
new file mode 100644
index 0000000..824e2d9
--- /dev/null
+++ b/src/vinted_scraper/models/vintedImage.py
@@ -0,0 +1,37 @@
+from dataclasses import dataclass
+from typing import List, Optional
+
+from .vintedHighResolution import VintedHighResolution
+from .vintedMedia import VintedMedia
+
+
+@dataclass
+class VintedImage:
+    id: Optional[str] = None
+    width: Optional[int] = None
+    height: Optional[int] = None
+    temp_uuid: Optional[str] = None
+    url: Optional[str] = None
+    dominant_color: Optional[str] = None
+    dominant_color_opaque: Optional[str] = None
+    thumbnails: Optional[List[VintedMedia]] = None
+    is_suspicious: Optional[bool] = None
+    orientation: Optional[str] = None
+    high_resolution: Optional[VintedHighResolution] = None
+    full_size_url: Optional[str] = None
+    is_hidden: Optional[bool] = None
+    image_no: Optional[int] = None
+    is_main: Optional[bool] = None
+
+    def __init__(self, json_data=None):
+        if json_data is not None:
+            self.__dict__.update(json_data)
+
+            high_resolution_data = json_data.get("high_resolution")
+            if high_resolution_data:
+                self.high_resolution = VintedHighResolution(high_resolution_data)
+
+            if "thumbnails" in json_data:
+                self.thumbnails = [
+                    VintedMedia(media) for media in json_data.get("thumbnails")
+                ]
diff --git a/src/vinted_scraper/models/vintedItem.py b/src/vinted_scraper/models/vintedItem.py
new file mode 100644
index 0000000..477784a
--- /dev/null
+++ b/src/vinted_scraper/models/vintedItem.py
@@ -0,0 +1,167 @@
+from dataclasses import dataclass
+from typing import List, Optional
+
+from .vintedBrand import VintedBrand
+from .vintedImage import VintedImage
+from .vintedPaymentMethod import VintedPaymentMethod
+from .vintedUser import VintedUser
+
+
+@dataclass
+class VintedItem:
+    id: Optional[str] = None
+    title: Optional[str] = None
+    price: Optional[float] = None
+    is_visible: Optional[int] = None
+    discount: Optional[str] = None
+    currency: Optional[str] = None
+    brand: Optional[VintedBrand] = None
+    is_for_swap: Optional[bool] = None
+    user: Optional[VintedUser] = None
+    url: Optional[str] = None
+    promoted: Optional[bool] = None
+    photo: Optional[VintedImage] = None
+    favourite_count: Optional[int] = None
+    is_favourite: Optional[bool] = None
+    badge: Optional[str] = None
+    conversion: Optional[str] = None
+    service_fee: Optional[float] = None
+    total_item_price: Optional[float] = None
+    view_count: Optional[int] = None
+    size_title: Optional[str] = None
+    content_source: Optional[str] = None
+    accepted_pay_in_methods: Optional[List[VintedPaymentMethod]] = None
+    user_id: Optional[str] = None
+    description: Optional[str] = None
+    brand_id: Optional[int] = None
+    size_id: Optional[str] = None
+    status_id: Optional[int] = None
+    disposal_conditions: Optional[int] = None
+    owner_id: Optional[str] = None
+    country_id: Optional[int] = None
+    catalog_id: Optional[int] = None
+    color1_id: Optional[str] = None
+    color2_id: Optional[str] = None
+    package_size_id: Optional[int] = None
+    is_hidden: Optional[int] = None
+    is_reserved: Optional[int] = None
+    reserved_for_user_id: Optional[str] = None
+    is_unisex: Optional[int] = None
+    is_closed: Optional[int] = None
+    active_bid_count: Optional[int] = None
+    moderation_status: Optional[int] = None
+    last_push_up_at: Optional[str] = None
+    package_size_standard: Optional[bool] = None
+    item_closing_action: Optional[str] = None
+    related_catalog_ids: Optional[List[int]] = None
+    related_catalogs_enabled: Optional[bool] = None
+    size: Optional[str] = None
+    composition: Optional[str] = None
+    extra_conditions: Optional[str] = None
+    is_for_sell: Optional[bool] = None
+    is_for_give_away: Optional[bool] = None
+    is_handicraft: Optional[bool] = None
+    is_processing: Optional[bool] = None
+    is_draft: Optional[bool] = None
+    label: Optional[str] = None
+    real_value_numeric: Optional[float] = None
+    original_price_numeric: Optional[float] = None
+    created_at_ts: Optional[str] = None
+    updated_at_ts: Optional[str] = None
+    user_updated_at_ts: Optional[str] = None
+    push_up_interval: Optional[int] = None
+    can_be_sold: Optional[bool] = None
+    can_feedback: Optional[bool] = None
+    item_reservation_id: Optional[str] = None
+    receiver_id: Optional[str] = None
+    promoted_until: Optional[str] = None
+    vas_gallery_promoted_until: Optional[str] = None
+    vas_gallery_promoted_created_at: Optional[str] = None
+    discount_price_numeric: Optional[float] = None
+    author: Optional[str] = None
+    book_title: Optional[str] = None
+    isbn: Optional[str] = None
+    measurement_width: Optional[str] = None
+    measurement_length: Optional[str] = None
+    measurement_unit: Optional[str] = None
+    transaction_permitted: Optional[bool] = None
+    video_game_rating_id: Optional[str] = None
+    item_attributes: Optional[List[str]] = None
+    is_story_uploaded: Optional[bool] = None
+    discount_price: Optional[float] = None
+    can_edit: Optional[bool] = None
+    can_delete: Optional[bool] = None
+    can_reserve: Optional[bool] = None
+    can_mark_as_sold: Optional[bool] = None
+    can_transfer: Optional[bool] = None
+    instant_buy: Optional[bool] = None
+    can_close: Optional[bool] = None
+    can_buy: Optional[bool] = None
+    can_bundle: Optional[bool] = None
+    can_ask_seller: Optional[bool] = None
+    can_favourite: Optional[bool] = None
+    user_login: Optional[str] = None
+    city_id: Optional[int] = None
+    city: Optional[str] = None
+    country: Optional[str] = None
+    is_mobile: Optional[bool] = None
+    bump_badge_visible: Optional[bool] = None
+    path: Optional[str] = None
+    created_at: Optional[str] = None
+    color1: Optional[str] = None
+    color2: Optional[str] = None
+    description_attributes: Optional[List[str]] = None
+    video_game_rating: Optional[str] = None
+    status: Optional[str] = None
+    performance: Optional[str] = None
+    stats_visible: Optional[bool] = None
+    can_push_up: Optional[bool] = None
+    can_vas_gallery_promote: Optional[bool] = None
+    vas_gallery_promoted: Optional[bool] = None
+    size_guide_faq_entry_id: Optional[str] = None
+    localization: Optional[str] = None
+    is_upload_story_button_visible: Optional[bool] = None
+    offline_verification: Optional[bool] = None
+    offline_verification_fee: Optional[float] = None
+
+    def __init__(self, json_data=None):
+        if json_data is not None:
+            self.__dict__.update(json_data)
+
+            if "user" in json_data:
+                self.user = VintedUser(json_data.get("user"))
+
+            if "photo" in json_data:
+                self.photo = VintedImage(json_data.get("photo"))
+
+            if "accepted_pay_in_methods" in json_data:
+                self.accepted_pay_in_methods = [
+                    VintedPaymentMethod(method)
+                    for method in json_data.get("accepted_pay_in_methods")
+                ]
+
+            if "brand_dto" in json_data:
+                self.brand = VintedBrand(json_data.get("brand_dto"))
+            elif "brand_title" in json_data:
+                self.brand = VintedBrand()
+                self.brand.title = json_data.get("brand_title")
+
+        if type(json_data.get("price")) is dict:
+            self.price = float(json_data.get("price")["amount"])
+            self.currency = json_data.get("price")["currency_code"]
+        else:
+            self.price = (
+                float(self.price) if isinstance(self.price, str) else self.price
+            )
+
+        self.service_fee = (
+            float(self.service_fee)
+            if isinstance(self.service_fee, str)
+            else self.service_fee
+        )
+
+        self.total_item_price = (
+            float(self.total_item_price)
+            if isinstance(self.total_item_price, str)
+            else self.total_item_price
+        )
diff --git a/src/vinted_scraper/models/vintedMedia.py b/src/vinted_scraper/models/vintedMedia.py
new file mode 100644
index 0000000..9d15893
--- /dev/null
+++ b/src/vinted_scraper/models/vintedMedia.py
@@ -0,0 +1,15 @@
+from dataclasses import dataclass
+from typing import Optional
+
+
+@dataclass
+class VintedMedia:
+    type: Optional[str] = None
+    url: Optional[str] = None
+    width: Optional[int] = None
+    height: Optional[int] = None
+    original_size: Optional[str] = None
+
+    def __init__(self, json_data=None):
+        if json_data is not None:
+            self.__dict__.update(json_data)
diff --git a/src/vinted_scraper/models/vintedPaymentMethod.py b/src/vinted_scraper/models/vintedPaymentMethod.py
new file mode 100644
index 0000000..3158dbc
--- /dev/null
+++ b/src/vinted_scraper/models/vintedPaymentMethod.py
@@ -0,0 +1,19 @@
+from dataclasses import dataclass
+from typing import Optional
+
+
+@dataclass
+class VintedPaymentMethod:
+    id: Optional[int] = None
+    code: Optional[str] = None
+    requires_credit_card: Optional[bool] = None
+    event_tracking_code: Optional[str] = None
+    icon: Optional[str] = None
+    enabled: Optional[bool] = None
+    translated_name: Optional[str] = None
+    note: Optional[str] = None
+    method_change_possible: Optional[bool] = None
+
+    def __init__(self, json_data=None):
+        if json_data is not None:
+            self.__dict__.update(json_data)
diff --git a/src/vinted_scraper/models/vintedUser.py b/src/vinted_scraper/models/vintedUser.py
new file mode 100644
index 0000000..ad7f759
--- /dev/null
+++ b/src/vinted_scraper/models/vintedUser.py
@@ -0,0 +1,107 @@
+from dataclasses import dataclass
+from typing import Any, Dict, List, Optional
+
+from .vintedBundleDiscount import VintedBundleDiscount
+from .vintedImage import VintedImage
+from .vintedPaymentMethod import VintedPaymentMethod
+
+
+@dataclass
+class VintedUser:
+    id: Optional[str] = None
+    login: Optional[str] = None
+    business: Optional[bool] = None
+    profile_url: Optional[str] = None
+    photo: Optional[VintedImage] = None
+    anon_id: Optional[str] = None
+    real_name: Optional[str] = None
+    email: Optional[str] = None
+    birthday: Optional[str] = None
+    gender: Optional[str] = None
+    item_count: Optional[int] = None
+    msg_template_count: Optional[int] = None
+    given_item_count: Optional[int] = None
+    taken_item_count: Optional[int] = None
+    favourite_topic_count: Optional[int] = None
+    forum_msg_count: Optional[int] = None
+    forum_topic_count: Optional[int] = None
+    followers_count: Optional[int] = None
+    following_count: Optional[int] = None
+    following_brands_count: Optional[int] = None
+    positive_feedback_count: Optional[int] = None
+    neutral_feedback_count: Optional[int] = None
+    negative_feedback_count: Optional[int] = None
+    meeting_transaction_count: Optional[int] = None
+    account_status: Optional[int] = None
+    email_bounces: Optional[str] = None
+    feedback_reputation: Optional[float] = None
+    feedback_count: Optional[int] = None
+    account_ban_date: Optional[str] = None
+    is_account_ban_permanent: Optional[bool] = None
+    is_forum_ban_permanent: Optional[bool] = None
+    is_on_holiday: Optional[bool] = None
+    is_publish_photos_agreed: Optional[bool] = None
+    expose_location: Optional[bool] = None
+    third_party_tracking: Optional[bool] = None
+    default_address: Optional[str] = None
+    last_loged_on_ts: Optional[str] = None
+    city_id: Optional[str] = None
+    city: Optional[str] = None
+    country_id: Optional[int] = None
+    country_code: Optional[str] = None
+    country_iso_code: Optional[str] = None
+    country_title: Optional[str] = None
+    contacts_permission: Optional[str] = None
+    contacts: Optional[str] = None
+    path: Optional[str] = None
+    moderator: Optional[bool] = None
+    is_catalog_moderator: Optional[bool] = None
+    is_catalog_role_marketing_photos: Optional[bool] = None
+    hide_feedback: Optional[bool] = None
+    can_post_big_forum_photos: Optional[bool] = None
+    allow_direct_messaging: Optional[bool] = None
+    bundle_discount: Optional[VintedBundleDiscount] = None
+    donation_configuration: Optional[str] = None
+    fundraiser: Optional[str] = None
+    has_ship_fast_badge: Optional[bool] = None
+    total_items_count: Optional[int] = None
+    about: Optional[str] = None
+    verification: Optional[Dict[str, Any]] = None
+    closet_promoted_until: Optional[str] = None
+    avg_response_time: Optional[str] = None
+    carrier_ids: Optional[List[int]] = None
+    carriers_without_custom_ids: Optional[List[int]] = None
+    locale: Optional[str] = None
+    updated_on: Optional[int] = None
+    is_hated: Optional[bool] = None
+    hates_you: Optional[bool] = None
+    is_favourite: Optional[bool] = None
+    share_profile_url: Optional[str] = None
+    facebook_user_id: Optional[str] = None
+    is_online: Optional[bool] = None
+    has_promoted_closet: Optional[bool] = None
+    can_view_profile: Optional[bool] = None
+    can_bundle: Optional[bool] = None
+    country_title_local: Optional[str] = None
+    last_loged_on: Optional[str] = None
+    accepted_pay_in_methods: Optional[List[VintedPaymentMethod]] = None
+    localization: Optional[str] = None
+    is_bpf_price_prominence_applied: Optional[bool] = None
+
+    def __init__(self, json_data=None):
+        if json_data is not None:
+            self.__dict__.update(json_data)
+
+            if "photo" in json_data:
+                self.photo = VintedImage(json_data.get("photo"))
+
+            if "bundle_discount" in json_data:
+                self.bundle_discount = VintedBundleDiscount(
+                    json_data.get("bundle_discount")
+                )
+
+            if "accepted_pay_in_methods" in json_data:
+                self.bundle_discount = [
+                    VintedPaymentMethod(method)
+                    for method in json_data.get("accepted_pay_in_methods")
+                ]
diff --git a/src/vinted_scraper/utils.py b/src/vinted_scraper/utils.py
new file mode 100644
index 0000000..715e91d
--- /dev/null
+++ b/src/vinted_scraper/utils.py
@@ -0,0 +1,10 @@
+import json
+import os
+import random
+
+
+def get_random_user_agent():
+    with open(os.path.join(os.path.dirname(__file__), "agents.json"), "r") as file:
+        data = json.load(file)
+        random_agent = random.choice(data)
+        return random_agent["ua"]
diff --git a/src/vinted_scraper/vintedScraper.py b/src/vinted_scraper/vintedScraper.py
new file mode 100644
index 0000000..bdfa680
--- /dev/null
+++ b/src/vinted_scraper/vintedScraper.py
@@ -0,0 +1,32 @@
+from typing import Dict, List, Optional
+
+from .models import VintedItem
+from .vintedWrapper import VintedWrapper
+
+
+class VintedScraper(VintedWrapper):
+    def __init__(self, baseurl: str, agent=None, session_cookie=None):
+        super().__init__(baseurl, agent, session_cookie)
+
+    def search(self, params: Optional[Dict] = None) -> List[VintedItem]:  # type: ignore
+        """
+        Search for items on Vinted.
+
+        :param params: an optional Dictionary with all the query parameters to append to the request.
+            Vinted supports a search without any parameters, but to perform a search,
+            you should add the `search_text` parameter.
+            Default value: None.
+        :return: A list of VintedItem instances representing search results.
+        """
+        return [VintedItem(item) for item in super().search(params)["items"]]
+
+    def item(self, item_id: str, params: Optional[Dict] = None) -> VintedItem:  # type: ignore
+        """
+        Retrieve details of a specific item on Vinted.
+
+        :param item_id: The unique identifier of the item to retrieve.
+        :param params: an optional Dictionary with all the query parameters to append to the request.
+            Default value: None.
+        :return: A VintedItem instance representing the item's details.
+        """
+        return VintedItem(super().item(item_id, params)["item"])
diff --git a/src/vinted_scraper/vintedWrapper.py b/src/vinted_scraper/vintedWrapper.py
new file mode 100644
index 0000000..0daa103
--- /dev/null
+++ b/src/vinted_scraper/vintedWrapper.py
@@ -0,0 +1,106 @@
+import json
+import re
+from typing import Any, Dict, Optional
+
+import requests
+
+from .utils import get_random_user_agent
+
+
+class VintedWrapper:
+    def __init__(self, baseurl: str, agent=None, session_cookie=None):
+        self.baseurl = baseurl[:-1] if baseurl.endswith("/") else baseurl
+
+        # Check if the URL is valid
+        if not re.match(
+            re.compile(r"^(https?://)?(www\.)?[\w.-]+\.\w{2,}$"), self.baseurl
+        ):
+            raise RuntimeError(f"{self.baseurl} is not a valid url, please check it!")
+
+        self.user_agent = agent if agent is not None else get_random_user_agent()
+        self.session_cookie = (
+            session_cookie if session_cookie is not None else self._fetch_cookie()
+        )
+
+    def _fetch_cookie(self) -> str:
+        """
+        Send an HTTP GET request to the self.base_url to fetch the session cookie.
+
+        :return: The session cookie extracted from the HTTP response headers.
+        :raises RuntimeError: If the session cookie cannot be fetched or doesn't match the expected format.
+
+        The method performs the following steps:
+        1. Sends an HTTP GET request to the base URL using the provided User-Agent header.
+        2. Retrieves the "Set-Cookie" header from the HTTP response.
+        3. Checks if the "Set-Cookie" header contains the expected session cookie format.
+        4. If a matching session cookie is found, it extracts and returns it.
+        5. If the session cookie cannot be fetched or doesn't match the expected format, it raises a RuntimeError.
+        """
+        response = requests.get(self.baseurl, headers={"User-Agent": self.user_agent})
+
+        session_cookie = response.headers.get("Set-Cookie")
+        if session_cookie and "secure, _vinted_fr_session=" in session_cookie:
+            return session_cookie.split("secure, _vinted_fr_session=")[1].split(";")[0]
+
+        raise RuntimeError(f"Cannot fetch session cookie from {self.baseurl}")
+
+    def search(self, params: Optional[Dict] = None) -> Dict[str, Any]:
+        """
+        Search for items on Vinted.
+
+        :param params: an optional Dictionary with all the query parameters to append to the request.
+            Vinted supports a search without any parameters, but to perform a search,
+            you should add the `search_text` parameter.
+            Default value: None.
+        :return: A Dict that contains the JSON response with the search results.
+        """
+        return self._curl("/catalog/items", params=params)
+
+    def item(self, item_id: str, params: Optional[Dict] = None) -> Dict[str, Any]:
+        """
+        Retrieve details of a specific item on Vinted.
+
+        :param item_id: The unique identifier of the item to retrieve.
+        :param params: an optional Dictionary with all the query parameters to append to the request.
+            Default value: None.
+        :return: A Dict that contains the JSON response with the item's details.
+        """
+        return self._curl(f"/items/{item_id}", params=params)
+
+    def _curl(self, endpoint: str, params: Optional[Dict] = None) -> Dict[str, Any]:
+        """
+        Send an HTTP GET request to the specified endpoint.
+
+        :param endpoint: The endpoint to make the request to.
+        :param params: An optional dictionary with query parameters to include in the request.
+                       Default value: None.
+        :return: A dictionary containing the parsed JSON response from the endpoint.
+        :raises RuntimeError: If the HTTP response status code is not 200, indicating an error.
+
+        The method performs the following steps:
+        1. Constructs the HTTP headers, including the User-Agent and session Cookie.
+        2. Sends an HTTP GET request to the specified endpoint with the given parameters.
+        3. Checks if the HTTP response status code is 200 (indicating success).
+        4. If the response status code is 200, it parses the JSON content of the response
+            and returns it as a dictionary.
+        5. If the response status code is not 200, it raises a RuntimeError with an error message.
+        """
+        headers = {
+            "User-Agent": self.user_agent,
+            "Cookie": f"_vinted_fr_session={self.session_cookie}",
+        }
+        response = requests.get(
+            f"{self.baseurl}/api/v2{endpoint}", params=params, headers=headers
+        )
+
+        if 200 == response.status_code:
+            return json.loads(response.content)
+        # TODO: Implement retry
+        # elif 401 == response.status_code:
+        #     # Fetch (maybe is expired?) the session cookie again and retry the API call
+        #     self.session_cookie = self._fetch_cookie()
+        #     return self._curl(endpoint, params)
+        else:
+            raise RuntimeError(
+                f"Cannot perform API call to endpoint {endpoint}, error code: {response.status_code}"
+            )


Test patch:
diff --git a/tests/samples/item_dummy.json b/tests/samples/item_dummy.json
index bb33eae..2a9a17e 100644
--- a/tests/samples/item_dummy.json
+++ b/tests/samples/item_dummy.json
@@ -1,80 +1,462 @@
 {
-  "id": "item id",
-  "title": "a Games",
-  "description": "a very beautiful game",
-  "is_reserved": 0,
-  "is_closed": 0,
-  "is_for_swap": false,
-  "active_bid_count": 0,
-  "item_closing_action": null,
-  "currency": "EUR",
-  "created_at_ts": "2023-07-06T16:40:45+02:00",
-  "photos": [
-    {
-      "id": "a photo id 1",
-      "url": "https://fakeulr.com",
-      "image_no": 1,
-      "dominant_color": "#787376",
-      "dominant_color_opaque": "#D7D5D6",
-      "is_main": true,
-      "is_suspicious": false,
-      "full_size_url": "https://fakeulr.com"
+  "item": {
+    "id": "game id",
+    "title": "a game ",
+    "brand_id": 425886,
+    "size_id": null,
+    "status_id": 2,
+    "disposal_conditions": 4,
+    "user_id": "user_id",
+    "owner_id": null,
+    "country_id": 14,
+    "catalog_id": 2321,
+    "color1_id": null,
+    "color2_id": null,
+    "package_size_id": 1,
+    "is_hidden": 0,
+    "is_reserved": 0,
+    "reserved_for_user_id": null,
+    "is_visible": 1,
+    "is_unisex": 0,
+    "is_closed": 0,
+    "active_bid_count": 0,
+    "favourite_count": 4,
+    "view_count": 19,
+    "moderation_status": 0,
+    "last_push_up_at": "2023-10-04T14:47:06-06:00",
+    "description": "Excellent fun! ",
+    "package_size_standard": true,
+    "item_closing_action": null,
+    "related_catalog_ids": [],
+    "related_catalogs_enabled": false,
+    "size": "",
+    "brand": "NO LABEL",
+    "composition": "",
+    "extra_conditions": "",
+    "is_for_sell": true,
+    "is_for_swap": false,
+    "is_for_give_away": false,
+    "is_handicraft": false,
+    "is_processing": false,
+    "is_draft": false,
+    "label": "NO LABEL",
+    "real_value_numeric": null,
+    "original_price_numeric": "4.5",
+    "currency": "USD",
+    "price_numeric": "4.5",
+    "created_at_ts": "2023-10-04T14:47:06-06:00",
+    "updated_at_ts": "2023-10-06T00:55:10-06:00",
+    "user_updated_at_ts": "2023-10-04T14:47:06-06:00",
+    "photos": [
+      {
+        "id": "photo id",
+        "image_no": 1,
+        "width": 600,
+        "height": 800,
+        "dominant_color": "#9A896A",
+        "dominant_color_opaque": "#E1DCD2",
+        "url": "https://fakeurl.com/a.jpeg",
+        "is_main": true,
+        "thumbnails": [
+          {
+            "type": "thumb70x100",
+            "url": "https://fakeurl.com/a.jpeg",
+            "width": 70,
+            "height": 100,
+            "original_size": null
+          }
+        ],
+        "high_resolution": {
+          "id": "hr id",
+          "timestamp": 1696452426,
+          "orientation": null
+        },
+        "is_suspicious": false,
+        "full_size_url": "https://fakeurl.com/a.jpeg",
+        "is_hidden": false,
+        "extra": {}
+      }
+    ],
+    "push_up_interval": 604800,
+    "can_be_sold": true,
+    "can_feedback": false,
+    "item_reservation_id": null,
+    "receiver_id": null,
+    "promoted_until": null,
+    "vas_gallery_promoted_until": null,
+    "vas_gallery_promoted_created_at": null,
+    "discount_price_numeric": null,
+    "author": null,
+    "book_title": null,
+    "isbn": null,
+    "measurement_width": null,
+    "measurement_length": null,
+    "measurement_unit": null,
+    "transaction_permitted": true,
+    "video_game_rating_id": null,
+    "item_attributes": [],
+    "is_story_uploaded": false,
+    "user": {
+      "id": "user id",
+      "anon_id": "user id",
+      "login": "username",
+      "real_name": null,
+      "email": null,
+      "birthday": null,
+      "gender": "F",
+      "item_count": 199,
+      "msg_template_count": 8,
+      "given_item_count": 1368,
+      "taken_item_count": 487,
+      "favourite_topic_count": 0,
+      "forum_msg_count": 0,
+      "forum_topic_count": 0,
+      "followers_count": 217,
+      "following_count": 9,
+      "following_brands_count": 12,
+      "positive_feedback_count": 543,
+      "neutral_feedback_count": 2,
+      "negative_feedback_count": 11,
+      "meeting_transaction_count": 0,
+      "account_status": 0,
+      "email_bounces": null,
+      "feedback_reputation": 0.9800000000000001,
+      "feedback_count": 556,
+      "account_ban_date": null,
+      "is_account_ban_permanent": null,
+      "is_forum_ban_permanent": null,
+      "is_on_holiday": false,
+      "is_publish_photos_agreed": true,
+      "expose_location": true,
+      "third_party_tracking": true,
+      "default_address": null,
+      "last_loged_on_ts": "2023-10-07T17:47:56-06:00",
+      "city_id": "city id",
+      "city": "SOUTH PORTLAND, ME",
+      "country_id": 14,
+      "country_code": "US",
+      "country_iso_code": "US",
+      "country_title": "United States",
+      "contacts_permission": null,
+      "contacts": null,
+      "photo": {
+        "id": "photo id",
+        "width": 600,
+        "height": 800,
+        "temp_uuid": null,
+        "url": "https://fakeurl.com/a.jpeg",
+        "dominant_color": "#31abc2",
+        "dominant_color_opaque": "#C1E6ED",
+        "thumbnails": [
+          {
+            "type": "thumb310",
+            "url": "https://fakeurl.com/a.jpeg",
+            "width": 310,
+            "height": 310,
+            "original_size": null
+          }
+        ],
+        "is_suspicious": false,
+        "orientation": null,
+        "high_resolution": {
+          "id": "hr id",
+          "timestamp": 1683722338,
+          "orientation": null
+        },
+        "full_size_url": "https://fakeurl.com/a.jpeg",
+        "is_hidden": false,
+        "extra": {}
+      },
+      "path": "/member/user_id-username",
+      "moderator": false,
+      "is_catalog_moderator": false,
+      "is_catalog_role_marketing_photos": false,
+      "hide_feedback": false,
+      "can_post_big_forum_photos": false,
+      "allow_direct_messaging": true,
+      "bundle_discount": {
+        "id": "bundle id",
+        "user_id": "user",
+        "enabled": true,
+        "minimal_item_count": 3,
+        "fraction": "0.1",
+        "discounts": [
+          {
+            "minimal_item_count": 2,
+            "fraction": "0.0"
+          },
+          {
+            "minimal_item_count": 3,
+            "fraction": "0.1"
+          },
+          {
+            "minimal_item_count": 5,
+            "fraction": "0.25"
+          }
+        ]
+      },
+      "donation_configuration": null,
+      "fundraiser": null,
+      "business": false,
+      "business_account": null,
+      "has_ship_fast_badge": false,
+      "total_items_count": 1567,
+      "about": "Spread kindness ",
+      "verification": {
+        "email": {
+          "valid": true,
+          "available": true
+        },
+        "facebook": {
+          "valid": false,
+          "verified_at": null,
+          "available": true
+        },
+        "google": {
+          "valid": false,
+          "verified_at": null,
+          "available": true
+        },
+        "phone": {
+          "valid": true,
+          "verified_at": "2019-11-18T20:19:24-07:00",
+          "available": true
+        }
+      },
+      "closet_promoted_until": null,
+      "avg_response_time": null,
+      "carrier_ids": [
+        2,
+        9,
+        10
+      ],
+      "carriers_without_custom_ids": [
+        2
+      ],
+      "locale": "en-us-fr",
+      "updated_on": 1696718008,
+      "is_hated": false,
+      "hates_you": false,
+      "is_favourite": false,
+      "profile_url": "https://fakeurl.com/",
+      "share_profile_url": "https://fakeurl.com/",
+      "facebook_user_id": null,
+      "is_online": false,
+      "has_promoted_closet": false,
+      "can_view_profile": true,
+      "can_bundle": true,
+      "country_title_local": "United States",
+      "last_loged_on": "yesterday 05:47 PM",
+      "accepted_pay_in_methods": [
+        {
+          "id": 1,
+          "code": "CREDIT_CARD",
+          "requires_credit_card": true,
+          "event_tracking_code": "cc",
+          "icon": "credit-card",
+          "enabled": true,
+          "translated_name": "Bank card",
+          "note": "Vinted never shares your payment information with the seller.",
+          "method_change_possible": true
+        },
+        {
+          "id": 22,
+          "code": "TRUSTLY",
+          "requires_credit_card": false,
+          "event_tracking_code": "trustly",
+          "icon": "trustly",
+          "enabled": true,
+          "translated_name": "Trustly",
+          "note": "Pay directly from your bank.",
+          "method_change_possible": true
+        },
+        {
+          "id": 16,
+          "code": "APPLE_PAY",
+          "requires_credit_card": false,
+          "event_tracking_code": "apple_pay",
+          "icon": "apple-pay",
+          "enabled": true,
+          "translated_name": "Apple Pay",
+          "note": "",
+          "method_change_possible": true
+        },
+        {
+          "id": 17,
+          "code": "GOOGLE_PAY",
+          "requires_credit_card": false,
+          "event_tracking_code": "google_pay",
+          "icon": "google-pay",
+          "enabled": true,
+          "translated_name": "Google Pay",
+          "note": "",
+          "method_change_possible": true
+        },
+        {
+          "id": 4,
+          "code": "SOFORT",
+          "requires_credit_card": false,
+          "event_tracking_code": "sofort",
+          "icon": "sofort",
+          "enabled": true,
+          "translated_name": "Sofort",
+          "note": "",
+          "method_change_possible": true
+        },
+        {
+          "id": 14,
+          "code": "DOTPAY",
+          "requires_credit_card": false,
+          "event_tracking_code": "dotpay",
+          "icon": "dotpay",
+          "enabled": true,
+          "translated_name": "Dotpay",
+          "note": "",
+          "method_change_possible": true
+        },
+        {
+          "id": 10,
+          "code": "MANGOPAY_PAYPAL",
+          "requires_credit_card": false,
+          "event_tracking_code": "mangopay_paypal",
+          "icon": "paypal",
+          "enabled": true,
+          "translated_name": "PayPal",
+          "note": "",
+          "method_change_possible": true
+        },
+        {
+          "id": 21,
+          "code": "PAYCONIQ",
+          "requires_credit_card": false,
+          "event_tracking_code": "payconiq",
+          "icon": "payconiq",
+          "enabled": true,
+          "translated_name": "Payconiq",
+          "note": "",
+          "method_change_possible": true
+        },
+        {
+          "id": 12,
+          "code": "P24",
+          "requires_credit_card": false,
+          "event_tracking_code": "p24",
+          "icon": "p24",
+          "enabled": true,
+          "translated_name": "Przelewy24",
+          "note": "",
+          "method_change_possible": true
+        },
+        {
+          "id": 18,
+          "code": "BLIK_DIRECT",
+          "requires_credit_card": false,
+          "event_tracking_code": "blik_direct",
+          "icon": "blik",
+          "enabled": true,
+          "translated_name": "Blik",
+          "note": "",
+          "method_change_possible": true
+        },
+        {
+          "id": 9,
+          "code": "IDEAL",
+          "requires_credit_card": false,
+          "event_tracking_code": "ideal",
+          "icon": "ideal",
+          "enabled": true,
+          "translated_name": "iDeal",
+          "note": "",
+          "method_change_possible": true
+        },
+        {
+          "id": 11,
+          "code": "WALLET",
+          "requires_credit_card": false,
+          "event_tracking_code": "wallet",
+          "icon": "wallet",
+          "enabled": true,
+          "translated_name": "Vinted Balance",
+          "note": "",
+          "method_change_possible": false
+        }
+      ],
+      "localization": "null",
+      "is_bpf_price_prominence_applied": true
     },
-    {
-      "id": "a photo id 2",
-      "url": "https://fakeulr.com",
-      "image_no": 1,
-      "dominant_color": "#787376",
-      "dominant_color_opaque": "#D7D5D6",
-      "is_main": true,
-      "is_suspicious": false,
-      "full_size_url": "https://fakeulr.com"
-    }
-  ],
-  "thumbnail": null,
-  "price": 20.0,
-  "favourite_count": 17,
-  "is_favourite": false,
-  "view_count": 78,
-  "url": "https://fakeulr.com",
-  "status": "Good",
-  "service_fee": 1.7,
-  "total_item_price": 21.7,
-  "user": {
-    "id": "a user id",
-    "login": "user",
-    "business": false,
-    "profile_url": null,
-    "photo": {
-      "id": "a photo id 3",
-      "url": "https://fakeulr.com",
-      "image_no": null,
-      "dominant_color": "#31abc2",
-      "dominant_color_opaque": "#C1E6ED",
-      "is_main": null,
-      "is_suspicious": false,
-      "full_size_url": "https://fakeulr.com"
+    "price": {
+      "amount": "4.5",
+      "currency_code": "USD"
     },
-    "item_count": 50,
-    "followers_count": 28,
-    "following_count": 9,
-    "positive_feedback_count": 25,
-    "neutral_feedback_count": 0,
-    "negative_feedback_count": 3,
-    "feedback_count": 28,
-    "feedback_reputation": 0.9199999999999999,
-    "city": "Rome",
-    "last_logged_on_ts": "2023-07-16T19:27:35+02:00",
-    "locale": "it-fr"
-  },
-  "brand_dto": {
-    "title": "Games",
-    "slug": "game",
-    "path": "/brand/game"
+    "discount_price": null,
+    "service_fee": "0.93",
+    "total_item_price": "5.43",
+    "can_edit": false,
+    "can_delete": false,
+    "can_reserve": false,
+    "can_mark_as_sold": false,
+    "can_transfer": false,
+    "instant_buy": true,
+    "can_close": false,
+    "can_buy": true,
+    "can_bundle": true,
+    "can_ask_seller": true,
+    "can_favourite": false,
+    "user_login": "username",
+    "city_id": 228552,
+    "city": "SOUTH PORTLAND, ME",
+    "country": "United States",
+    "promoted": false,
+    "is_mobile": false,
+    "bump_badge_visible": false,
+    "brand_dto": {
+      "id": "brand id",
+      "title": "NO LABEL",
+      "slug": "no-label",
+      "favourite_count": 88173,
+      "pretty_favourite_count": "88.2K",
+      "item_count": 1089206,
+      "pretty_item_count": "1.1M",
+      "is_visible_in_listings": true,
+      "requires_authenticity_check": false,
+      "is_luxury": false,
+      "path": "/brand/brand-id-no-label",
+      "url": "https://fakeurl.com/",
+      "is_favourite": false
+    },
+    "path": "/items/game-id",
+    "url": "https://fakeurl.com/",
+    "accepted_pay_in_methods": [
+      {
+        "id": 1,
+        "code": "CREDIT_CARD",
+        "requires_credit_card": true,
+        "event_tracking_code": "cc",
+        "icon": "credit-card",
+        "enabled": true,
+        "translated_name": "Bank card",
+        "note": "Vinted never shares your payment information with the seller.",
+        "method_change_possible": true
+      }
+    ],
+    "created_at": "10/04 02:47 PM",
+    "color1": null,
+    "color2": null,
+    "description_attributes": [],
+    "video_game_rating": null,
+    "status": "Very good",
+    "is_favourite": false,
+    "performance": null,
+    "stats_visible": false,
+    "can_push_up": false,
+    "can_vas_gallery_promote": false,
+    "vas_gallery_promoted": false,
+    "badge": null,
+    "size_guide_faq_entry_id": null,
+    "localization": "null",
+    "is_upload_story_button_visible": false,
+    "offline_verification": false,
+    "offline_verification_fee": null
   },
-  "color1": null,
-  "color2": null,
-  "description_attributes": [],
-  "localization": "none"
-}
-
+  "code": 0
+}
\ No newline at end of file
diff --git a/tests/samples/search_item_dummy.json b/tests/samples/search_item_dummy.json
index ec8f71f..92e0b9f 100644
--- a/tests/samples/search_item_dummy.json
+++ b/tests/samples/search_item_dummy.json
@@ -1,37 +1,97 @@
 {
-  "id": "item id",
-  "title": "a games",
-  "price": {
-    "amount": "15.0", "currency_code": "USD"
-  },
-  "is_visible": 1,
-  "discount": null,
-  "currency": "EUR",
-  "brand_title": "Games",
-  "is_for_swap": false,
-  "user": {
-    "id": "user id",
-    "login": "user",
-    "business": false,
-    "profile_url": "https://www.fakeurl.com",
-    "photo": {
-      "id": "photo id",
-      "width": 450,
-      "height": 800,
-      "temp_uuid": null,
-      "url": "https://fakeurl.com",
-      "is_hidden": false,
-      "extra": {}
-    },
-    "favourite_count": 36,
-    "is_favourite": false,
-    "favourite_group_id": null,
-    "badge": null,
-    "conversion": null,
-    "service_fee": {"amount": "1.45", "currency_code": "USD"},
-    "total_item_price": {"amount": "16.45", "currency_code": "USD"},
-    "view_count": 0,
-    "size_title": "",
-    "content_source": "search"
-  }
-}
+  "items": [
+    {
+      "id": "item id",
+      "title": "a game ",
+      "price": "4.5",
+      "is_visible": 1,
+      "discount": null,
+      "currency": "USD",
+      "brand_title": "NO LABEL",
+      "is_for_swap": false,
+      "user": {
+        "id": "user id",
+        "login": "username",
+        "business": false,
+        "profile_url": "https://www.fakeurl.com",
+        "photo": {
+          "id": "img id",
+          "width": 600,
+          "height": 800,
+          "temp_uuid": null,
+          "url": "https://www.fakeurl.com/a.jpg",
+          "dominant_color": "#31abc2",
+          "dominant_color_opaque": "#C1E6ED",
+          "thumbnails": [
+            {
+              "type": "thumb310",
+              "url": "https://www.fakeurl.com/a.jpg",
+              "width": 310,
+              "height": 310,
+              "original_size": null
+            }
+          ],
+          "is_suspicious": false,
+          "orientation": null,
+          "high_resolution": {
+            "id": "high_resolution_id",
+            "timestamp": 1683722338,
+            "orientation": null
+          },
+          "full_size_url": "https://www.fakeurl.com/a.jpg",
+          "is_hidden": false,
+          "extra": {}
+        }
+      },
+      "url": "https://www.fakeurl.com/item/item_id",
+      "promoted": false,
+      "photo": {
+        "id": "photo_id",
+        "image_no": 1,
+        "width": 600,
+        "height": 800,
+        "dominant_color": "#9A896A",
+        "dominant_color_opaque": "#E1DCD2",
+        "url": "https://www.fakeurl.com/a.jpg",
+        "is_main": true,
+        "thumbnails": [
+          {
+            "type": "thumb70x100",
+            "url": "https://www.fakeurl.com/a.jpg",
+            "width": 70,
+            "height": 100,
+            "original_size": null
+          }
+        ],
+        "high_resolution": {
+          "id": "high_resolution_id",
+          "timestamp": 1696452426,
+          "orientation": null
+        },
+        "is_suspicious": false,
+        "full_size_url": "https://www.fakeurl.com/a.jpg",
+        "is_hidden": false,
+        "extra": {}
+      },
+      "favourite_count": 4,
+      "is_favourite": false,
+      "badge": null,
+      "conversion": null,
+      "service_fee": "0.93",
+      "total_item_price": "5.43",
+      "view_count": 0,
+      "size_title": "",
+      "content_source": "search",
+      "search_tracking_params": {
+        "score": 39.74531,
+        "matched_queries": [
+          "next_search",
+          "full_text_search",
+          "interpretation_0",
+          "boost_by_newest_items",
+          "reranked_by_score"
+        ]
+      }
+    }
+  ]
+}
\ No newline at end of file
diff --git a/tests/test_get_item.py b/tests/test_get_item.py
deleted file mode 100644
index df0b948..0000000
--- a/tests/test_get_item.py
+++ /dev/null
@@ -1,61 +0,0 @@
-# import unittest
-# from unittest.mock import MagicMock, Mock, patch
-#
-# import requests
-#
-# # isort: split
-# from src.vinted_scraper import VintedItem, get_item, get_raw_item
-# from tests.utils import get_dummy_item_data, get_empty_data
-#
-#
-# class MyTestCase(unittest.TestCase):
-#     def test_get_raw_item(self):
-#         """
-#         In this use case we test if the raw search can extract data from the HTML response.
-#         """
-#         data, html = get_dummy_item_data()
-#         mock_response = MagicMock(spec=requests.Response)
-#         mock_response.status_code = 200
-#         mock_response.content = html
-#         with patch("requests.get", return_value=mock_response):
-#             self.assertEqual(data, get_raw_item("https://www.fakeurl.com"))
-#
-#     def test_get_item(self):
-#         """
-#         In this use case we test if the search can extract data from the HTML response.
-#         """
-#         data, html = get_dummy_item_data()
-#         mock_response = MagicMock(spec=requests.Response)
-#         mock_response.status_code = 200
-#         mock_response.content = html
-#         with patch("requests.get", return_value=mock_response):
-#             self.assertEqual(VintedItem(data), get_item("https://www.fakeurl.com"))
-#
-#     def test_missing_tag_error(self):
-#         """
-#         In this use case we test when we can't find the right script HTML tag, in the HTML response.
-#         """
-#         mock_response = MagicMock(spec=requests.Response)
-#         mock_response.status_code = 200
-#         mock_response.content = get_empty_data()
-#
-#         with patch("requests.get", return_value=mock_response):
-#             self.assertRaises(
-#                 RuntimeError, lambda: get_raw_item("https://www.fakeurl.com")
-#             )
-#
-#     def test_status_code_error(self):
-#         """
-#         In this use case we test when we receive a response that has not a 200 status code.
-#         """
-#         mock_response = Mock()
-#         mock_response.status_code = 404
-#
-#         with patch("requests.get", return_value=mock_response):
-#             self.assertRaises(
-#                 RuntimeError, lambda: get_raw_item("https://www.fakeurl.com")
-#             )
-#
-#
-# if __name__ == "__main__":
-#     unittest.main()
diff --git a/tests/test_quickstart.py b/tests/test_quickstart.py
index 4e1ce26..30112d9 100644
--- a/tests/test_quickstart.py
+++ b/tests/test_quickstart.py
@@ -1,30 +1,42 @@
 import unittest
+from time import sleep
 
-from src.vinted_scraper import get_item, get_raw_item, raw_search, search
+from src.vinted_scraper.vintedScraper import VintedScraper
+from src.vinted_scraper.vintedWrapper import VintedWrapper
 
 
-class MyTestCase(unittest.TestCase):
-    def test_quick_start(self):
+class TestQuickstarts(unittest.TestCase):
+    def test_raw_quick_start(self):
         """
-        Ensure that the function doesn't raise any exceptions
+        Ensure that the wrapper quickstart doesn't raise any exceptions
         """
         try:
+            wrapper = VintedWrapper("https://www.vinted.com")
             params = {"search_text": "board games"}
-            items = search("https://www.vinted.com/catalog", params)
-            item = items[0]
-            get_item(item.url)
+            items = wrapper.search(params)
+            if len(items["items"]) > 0:
+                wrapper.item(items["items"][0]["id"])
+            else:
+                # when you call multiple times the search sometimes returns an empty result
+                sleep(1)
+                self.test_raw_quick_start()
         except Exception as e:
             self.fail(f"Quick raised an exception: {e}")
 
-    def test_raw_quick_start(self):
+    def test_quick_start(self):
         """
-        Ensure that the function doesn't raise any exceptions
+        Ensure that the scrapper quickstart doesn't raise any exceptions
         """
         try:
+            scraper = VintedScraper("https://www.vinted.com")
             params = {"search_text": "board games"}
-            items = raw_search("https://www.vinted.com/catalog", params)
-            item = items[0]
-            get_raw_item(item["url"])
+            items = scraper.search(params)
+            if len(items) > 0:
+                scraper.item(items[0].id)
+            else:
+                # when you call multiple times the search sometimes returns an empty result
+                sleep(1)
+                self.test_quick_start()
         except Exception as e:
             self.fail(f"Quick raised an exception: {e}")
 
diff --git a/tests/test_search.py b/tests/test_search.py
deleted file mode 100644
index 5e41468..0000000
--- a/tests/test_search.py
+++ /dev/null
@@ -1,69 +0,0 @@
-import unittest
-from unittest.mock import MagicMock, Mock, patch
-
-import requests
-
-# isort: split
-from src.vinted_scraper import VintedItem, raw_search, search
-from tests.utils import get_dummy_search_data, get_empty_data
-
-
-class MyTestCase(unittest.TestCase):
-    def setUp(self) -> None:
-        self.response_200 = MagicMock(spec=requests.Response)
-        self.response_200.status_code = 200
-        self.response_200.headers = {}
-
-    def test_raw_search_error(self):
-        """
-        In this use case we test if the raw search can extract data from the HTML response.
-        """
-        data, html = get_dummy_search_data()
-        self.response_200.content = html
-
-        with patch("requests.get", return_value=self.response_200):
-            params = {"search_text": "unit_test"}
-            self.assertEqual([data], raw_search("https://www.fakeurl.com", params))
-
-    def test_search_error(self):
-        """
-        In this use case we test if the search can extract data from the HTML response.
-        """
-        data, html = get_dummy_search_data()
-        self.response_200.content = html
-
-        with patch("requests.get", return_value=self.response_200):
-            params = {"search_text": "unit_test"}
-            self.assertEqual(
-                [VintedItem(data)], search("https://www.fakeurl.com", params)
-            )
-
-    def test_missing_tag_error(self):
-        """
-        In this use case we test when we can't find the right script HTML tag, in the HTML response.
-        """
-        self.response_200.content = get_empty_data()
-
-        with patch("requests.get", return_value=self.response_200):
-            params = {"search_text": "unit_test"}
-            self.assertRaises(
-                RuntimeError, lambda: raw_search("https://www.fakeurl.com", params)
-            )
-
-    def test_status_code_error(self):
-        """
-        In this use case we test when we receive a response that has not a 200 status code.
-        """
-        mock_response = Mock()
-        mock_response.status_code = 404
-        mock_response.headers = {}
-
-        with patch("requests.get", return_value=mock_response):
-            params = {"search_text": "unit_test"}
-            self.assertRaises(
-                RuntimeError, lambda: raw_search("https://www.fakeurl.com", params)
-            )
-
-
-if __name__ == "__main__":
-    unittest.main()
diff --git a/tests/test_user_agent.py b/tests/test_utils.py
similarity index 60%
rename from tests/test_user_agent.py
rename to tests/test_utils.py
index 52f1c32..57744c4 100644
--- a/tests/test_user_agent.py
+++ b/tests/test_utils.py
@@ -1,12 +1,12 @@
 import unittest
 
-from src.vinted_scraper import get_random_user_agent
+from src.vinted_scraper.utils import get_random_user_agent
 
 
-class MyTestCase(unittest.TestCase):
+class TestUtils(unittest.TestCase):
     def test_random_user_agent(self):
         """
-        Ensure that the function doesn't raise any exceptions
+        Ensure that the user agent function doesn't raise any exceptions
         """
         try:
             get_random_user_agent()
diff --git a/tests/test_vinted.py b/tests/test_vinted.py
new file mode 100644
index 0000000..e89589e
--- /dev/null
+++ b/tests/test_vinted.py
@@ -0,0 +1,52 @@
+import unittest
+from unittest.mock import patch
+
+from src.vinted_scraper.vintedScraper import VintedScraper
+
+# isort: split
+from src.vinted_scraper.vintedWrapper import VintedWrapper
+from tests.utils import get_200_response
+
+
+class TestVinted(unittest.TestCase):
+    def test_init_valid_url(self):
+        """
+        Ensure that the initializer accepts a valid URL
+        """
+        baseurl = "https://fakeurl.com"
+
+        with patch("requests.get", return_value=get_200_response()):
+            wrapper = VintedWrapper(baseurl)
+            self.assertEqual(wrapper.baseurl, baseurl)
+
+        with patch("requests.get", return_value=get_200_response()):
+            wrapper = VintedScraper(baseurl)
+            self.assertEqual(wrapper.baseurl, baseurl)
+
+    def test_init_invalid_url(self):
+        """
+        Ensure that the initializer raises an error for an invalid URL
+        """
+        baseurl = "invalid_url"
+
+        with self.assertRaises(RuntimeError):
+            VintedWrapper(baseurl)
+
+        with self.assertRaises(RuntimeError):
+            VintedScraper(baseurl)
+
+    def test_init_invalid_cookie(self):
+        """
+        Ensure that the initializer raises an error if it can find the session cookie
+        """
+        baseurl = "https://fakeurl.com"
+        response = get_200_response()
+        response.headers = {}
+
+        with self.assertRaises(RuntimeError):
+            with patch("requests.get", return_value=response):
+                VintedWrapper(baseurl)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/test_vinted_item.py b/tests/test_vinted_item.py
new file mode 100644
index 0000000..789a8ca
--- /dev/null
+++ b/tests/test_vinted_item.py
@@ -0,0 +1,53 @@
+import json
+import unittest
+from unittest.mock import Mock, patch
+
+from src.vinted_scraper.models import VintedItem
+
+# isort: split
+from tests.utils import _read_data_from_file, get_200_response, get_scraper, get_wrapper
+
+
+class TestItem(unittest.TestCase):
+    def setUp(self):
+        self.baseurl = "https://fakeurl.com"
+        self.response_200 = get_200_response()
+        self.wrapper = get_wrapper(self.baseurl)
+        self.scraper = get_scraper(self.baseurl)
+
+    def test_item_with_params(self):
+        """
+        Test if the item method call the _curl method with the right params
+        """
+        obj = {"item": {}}
+        item_id = "id"
+        self.wrapper._curl = Mock(return_value=obj)
+
+        self.wrapper._curl = Mock(return_value=obj)
+        result = self.wrapper.item(item_id)
+        self.wrapper._curl.assert_called_once_with(f"/items/{item_id}", params=None)
+        self.assertEqual(result, obj)
+
+    def test_get_item(self):
+        """
+        Test the item method
+        """
+        data = _read_data_from_file("item_dummy")
+        self.response_200.content = json.dumps(data)
+
+        with patch("requests.get", return_value=self.response_200):
+            self.assertEqual(data, self.wrapper.item("id"))
+
+        with patch("requests.get", return_value=self.response_200):
+            self.assertEqual(VintedItem(data["item"]), self.scraper.item("id"))
+
+    def test_status_code_error(self):
+        """
+        Test the case when a status code different from 200 is returned by the web service
+        """
+        mock_response = Mock()
+        mock_response.status_code = 404
+        mock_response.headers = {}
+
+        with patch("requests.get", return_value=mock_response):
+            self.assertRaises(RuntimeError, lambda: self.wrapper.item("id"))
diff --git a/tests/test_vinted_search.py b/tests/test_vinted_search.py
new file mode 100644
index 0000000..52629ac
--- /dev/null
+++ b/tests/test_vinted_search.py
@@ -0,0 +1,65 @@
+import json
+import unittest
+from unittest.mock import Mock, patch
+
+from src.vinted_scraper.models import VintedItem
+
+# isort: split
+from tests.utils import _read_data_from_file, get_200_response, get_scraper, get_wrapper
+
+
+class TestVintedSearch(unittest.TestCase):
+    def setUp(self):
+        self.baseurl = "https://fakeurl.com"
+        self.response_200 = get_200_response()
+        self.wrapper = get_wrapper(self.baseurl)
+        self.scrapper = get_scraper(self.baseurl)
+
+    def test_raw_search_with_params(self):
+        """
+        Test if the search method call the _curl method with the right params
+        """
+        obj = {"items": []}
+        self.wrapper._curl = Mock(return_value=obj)
+
+        params = {"search_text": "games"}
+
+        for x in [params, None]:
+            self.wrapper._curl = Mock(return_value=obj)
+            result = self.wrapper.search(x)
+            self.wrapper._curl.assert_called_once_with("/catalog/items", params=x)
+            self.assertEqual(result, obj)
+
+    def test_search_error(self):
+        """
+        Test the search method.
+        """
+        data = _read_data_from_file("search_item_dummy")
+        self.response_200.content = json.dumps(data)
+
+        with patch("requests.get", return_value=self.response_200):
+            params = {"search_text": "unit_test"}
+            self.assertEqual(data, self.wrapper.search(params))
+            self.assertEqual(
+                [VintedItem(item) for item in data["items"]],
+                self.scrapper.search(params),
+            )
+
+    def test_status_code_error(self):
+        """
+        Test the case when a status code different from 200 is returned by the web service
+        """
+        mock_response = Mock()
+        mock_response.status_code = 404
+        mock_response.headers = {}
+
+        with patch("requests.get", return_value=mock_response):
+            self.assertRaises(RuntimeError, lambda: self.wrapper.search())
+
+    # TODO: implement retry first
+    # def test_cookies_retry(self):
+    #     try:
+    #         wrapper = VintedWrapper(self.baseurl, session_cookie="invalid_cookie")
+    #         wrapper.search()
+    #     except Exception as e:
+    #         self.fail(f"exception: {e}")
diff --git a/tests/utils.py b/tests/utils.py
index e3f7d1e..3c31e32 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -1,53 +1,40 @@
 import json
 import os
-from typing import Dict, Tuple
+from typing import Dict
+from unittest.mock import MagicMock, patch
 
+import requests
 
-def get_dummy_search_data() -> Tuple[Dict, bytes]:
+# isort: split
+from src.vinted_scraper import VintedScraper, VintedWrapper
+
+
+def get_200_response() -> MagicMock:
     """
-    Load dummy search data
+    :return: a mocked 200 response using MagicMock already configured
     """
-    data = _read_data_from_file("search_item_dummy")
-    return (
-        data,
-        f"""
-             <script type="application/json" data-js-react-on-rails-store="MainStore">
-            {{
-                "items": {{
-                    "catalogItems": {{
-                        "byId": {{
-                            "dummyId": {json.dumps(data)}
-                        }}
-                    }}
-                }}
-            }}
-            </script>
-""".encode(),
-    )
+    response_200 = MagicMock(spec=requests.Response)
+    response_200.status_code = 200
+    response_200.headers = {"Set-Cookie": "secure, _vinted_fr_session=test"}
+    return response_200
 
 
-def get_dummy_item_data() -> Tuple[Dict, bytes]:
+def get_wrapper(url: str) -> VintedWrapper:
     """
-    Load dummy item data
+    :param url: a valid https url like: "https://fakeurl.com"
+    :return: a VintedWrapper instance for testing
     """
-    data = _read_data_from_file("item_dummy")
-    return (
-        data,
-        f"""
-         <script type="application/json" data-component-name="ItemDetails">
-        {{
-            "item": {json.dumps(data)}
-        }}
-        </script>
-""".encode(),
-    )
+    with patch("requests.get", return_value=get_200_response()):
+        return VintedWrapper(url)
 
 
-def get_empty_data() -> bytes:
+def get_scraper(url: str) -> VintedWrapper:
     """
-    Get an empty HTML where no data can be scraped
+    :param url: a valid https url like: "https://fakeurl.com"
+    :return: a VintedScraper instance for testing
     """
-    return "<html><body><script><!-- --></script></body></html>".encode()
+    with patch("requests.get", return_value=get_200_response()):
+        return VintedScraper(url)
 
 
 def _read_data_from_file(filename: str) -> Dict: