diff --git a/README.md b/README.md
index 3cd0a9f..d935874 100644
--- a/README.md
+++ b/README.md
@@ -6,186 +6,25 @@
[](https://img.shields.io/github/commit-activity/m/theuerc/pyravelry)
[](https://img.shields.io/github/license/theuerc/pyravelry)
-This is python wrapper for the Ravelry API, which is a database of knitting / crocheting patterns.
+This is python wrapper for the Ravelry API (a database of knitting / crocheting patterns).
- **Github repository**:
- **Documentation**
+- **Official Ravelry API Documentation**
-Documentation for the Ravelry API--including how to get a http read-only API key--can be found here after logging in or making an account: https://www.ravelry.com/api
+Use of this API wrapper requires a [Ravelry Account](https://www.ravelry.com/) and a username and apikey as specified in the [HTTP Basic Auth](https://www.ravelry.com/api#authenticating) section of the Ravelry API Documentation.
-This is the list of endpoints I am working through:
-
-- [x] /color_families
-- [ ] /current_user
-- [x] /fiber_attributes
-- [x] /fiber_categories
-- [x] /search
-- [x] /yarn_weights
-- [ ] /app (base level)
-- [ ] /bundled_items (base level)
-- [ ] /bundles (base level)
-- [ ] /carts (base level)
-- [ ] /comments (base level)
-- [ ] /deliveries (base level)
-- [ ] /designers (base level)
-- [ ] /drafts (base level)
-- [ ] /favorites (base level)
-- [ ] /fiber (base level)
-- [ ] /fiber_attributes_groups (base level)
-- [ ] /forum_posts (base level)
-- [ ] /forums (base level)
-- [ ] friends (base level)
-
-This is the list of pydantic models I am using for validating the output of the API:
-
-- [ ] Activity
-- [ ] Ad
-- [ ] AttributeGroup
-- [ ] Bookmark
-- [ ] Bundle
-- [ ] BundledItem
-- [ ] Business
-- [ ] Cart
-- [ ] CartItem
-- [ ] Collection
-- [x] ColorFamily
-- [x] Colorway
-- [ ] CombinedCart
-- [ ] ComponentYarn
-- [x] Craft
-- [ ] Delivery
-- [ ] Document
-- [ ] DownloadLink
-- [ ] DraftComponentYarn
-- [ ] DraftErrataLink
-- [ ] DraftNeedleSize
-- [ ] DraftPattern
-- [ ] DraftPatternYarn
-- [x] FiberAttribute
-- [x] FiberAttributeGroup
-- [x] FiberCategory
-- [ ] FiberPack
-- [ ] FiberStash
-- [x] FiberType
-- [ ] Forum
-- [ ] ForumPost
-- [ ] ForumPreference
-- [ ] ForumSet
-- [ ] ForumStatisticSummary
-- [ ] Friendship
-- [ ] Group
-- [ ] InStoreSale
-- [ ] Invoice
-- [ ] InvoiceLineItem
-- [ ] Language
-- [ ] Message
-- [ ] NeedleRecord
-- [ ] NeedleSize
-- [ ] NeedleType
-- [ ] Pack
-- [ ] Pattern
-- [ ] PatternAttribute
-- [ ] PatternAuthor
-- [ ] PatternCategory
-- [ ] PatternClassification
-- [ ] PatternLanguage
-- [ ] PatternNeedleSize
-- [ ] PatternSource
-- [ ] PatternSourceType
-- [ ] PatternTagging
-- [x] Photo
-- [ ] Printing
-- [ ] Product
-- [ ] ProductAttachment
-- [ ] ProductNotification
-- [ ] Project
-- [ ] ProjectStatus
-- [ ] QueuedProject
-- [ ] QueuedStash
-- [ ] Saleable
-- [ ] SavedSearch
-- [ ] Shop
-- [ ] ShopCustomer
-- [ ] ShopSchedule
-- [ ] SocialSite
-- [ ] Stash
-- [ ] StashStatus
-- [ ] Store
-- [ ] Tool
-- [ ] Topic
-- [ ] UnifiedStash
-- [ ] User
-- [ ] UserSite
-- [ ] Volume
-- [ ] VolumeAttachment
-- [x] Yarn
-- [x] YarnAttributeGroup
-- [x] YarnCompany
-- [x] YarnCountry
-- [x] YarnFiber
-- [x] YarnProvenance
-- [x] YarnWeight
-
----
-
-Everything below this point will be deleted before the package is published.
-
-### 1. Create a New Repository
-
-First, create a repository on GitHub with the same name as this project, and then run the following commands:
+Quick Start:
```bash
-git init -b main
-git add .
-git commit -m "init commit"
-git remote add origin git@github.com:theuerc/pyravelry.git
-git push -u origin main
+$pip install pyravelry
+$python -i
+>>> from pyravelry import Client, Settings
+>>> settings = Settings(RAVELRY_USERNAME=..., RAVELRY_API_KEY=...)
+>>> client = Client(settings=settings)
+>>> results = client.search.query(query="merino", limit=10, types="Yarn")
+>>> results[0].title
+'MerinoSeide'
```
-### 2. Set Up Your Development Environment
-
-Then, install the environment and the pre-commit hooks with
-
-```bash
-make install
-```
-
-This will also generate your `uv.lock` file
-
-### 3. Run the pre-commit hooks
-
-Initially, the CI/CD pipeline might be failing due to formatting issues. To resolve those run:
-
-```bash
-uv run pre-commit run -a
-```
-
-### 4. Commit the changes
-
-Lastly, commit the changes made by the two steps above to your repository.
-
-```bash
-git add .
-git commit -m 'Fix formatting issues'
-git push origin main
-```
-
-You are now ready to start development on your project!
-The CI/CD pipeline will be triggered when you open a pull request, merge to main, or when you create a new release.
-
-To finalize the set-up for publishing to PyPI, see [here](https://fpgmaas.github.io/cookiecutter-uv/features/publishing/#set-up-for-pypi).
-For activating the automatic documentation with MkDocs, see [here](https://fpgmaas.github.io/cookiecutter-uv/features/mkdocs/#enabling-the-documentation-on-github).
-To enable the code coverage reports, see [here](https://fpgmaas.github.io/cookiecutter-uv/features/codecov/).
-
-## Releasing a new version
-
-- Create an API Token on [PyPI](https://pypi.org/).
-- Add the API Token to your projects secrets with the name `PYPI_TOKEN` by visiting [this page](https://github.com/theuerc/pyravelry/settings/secrets/actions/new).
-- Create a [new release](https://github.com/theuerc/pyravelry/releases/new) on Github.
-- Create a new tag in the form `*.*.*`.
-
-For more details, see [here](https://fpgmaas.github.io/cookiecutter-uv/features/cicd/#how-to-trigger-a-release).
-
----
-
-Repository initiated with [fpgmaas/cookiecutter-uv](https://github.com/fpgmaas/cookiecutter-uv).
+More information can be found in the [pyravelry documentation](https://theuerc.github.io/pyravelry/).
diff --git a/docs/index.md b/docs/index.md
index 6bffcf7..84e37f2 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -5,4 +5,163 @@
[](https://img.shields.io/github/commit-activity/m/theuerc/pyravelry)
[](https://img.shields.io/github/license/theuerc/pyravelry)
-This is python wrapper for the Ravelry API, which is a database of knitting / crocheting patterns.
+This is python wrapper for the Ravelry API (a database of knitting / crocheting patterns).
+
+Use of this API wrapper requires a [Ravelry Account](https://www.ravelry.com/) and a username and apikey as specified in the [HTTP Basic Auth](https://www.ravelry.com/api#authenticating) section of the Ravelry API Documentation.
+
+Quick Start:
+
+```bash
+$pip install pyravelry
+$python -i
+>>> from pyravelry import Client, Settings
+>>> settings = Settings(RAVELRY_USERNAME=..., RAVELRY_API_KEY=...)
+>>> client = Client(settings=settings)
+>>> results = client.search.query(query="merino", limit=10, types="Yarn")
+>>> results[0].title
+'MerinoSeide'
+```
+
+I've checked the API endpoints / models that are currently supported in the official documentation. The crossed ones are not planned to ever be supported:
+
+Endpoints:
+
+- [x] /color_families
+- [ ] ~~/current_user~~
+- [x] /fiber_attributes
+- [x] /fiber_categories
+- [x] /search
+- [x] /yarn_weights
+- [ ] ~~/app (base level)~~
+- [ ] /bundled_items (base level)
+- [ ] /bundles (base level)
+- [ ] ~~/carts (base level)~~
+- [ ] ~~/comments (base level)~~ # doesn't allow retrieval of anyone else's comments.
+- [ ] ~~/deliveries (base level)~~
+- [ ] /designers (base level)
+- [ ] ~~/drafts (base level)~~
+- [ ] ~~/favorites (base level)~~
+- [ ] /fiber (base level)
+- [ ] /fiber_attributes_groups (base level)
+- [ ] /forum_posts (base level)
+- [ ] /forums (base level)
+- [ ] ~~/friends (base level)~~
+- [ ] /groups
+- [ ] ~~/in_store_sales~~
+- [ ] /languages
+- [ ] ~~/library~~
+- [ ] ~~/messages~~
+- [ ] /needles
+- [ ] ~~/packs~~
+- [ ] /pages
+- [ ] /pattern_attributes
+- [ ] /pattern_categories
+- [ ] /pattern_source_types
+- [ ] /pattern_sources
+- [ ] /patterns
+- [ ] /people
+- [ ] /photos
+- [ ] /product_attachments
+- [ ] /products
+- [ ] /projects
+- [ ] ~~/queue~~
+- [ ] ~~/saved_searches~~
+- [ ] ~~/shops~~
+- [ ] ~~/stash~~
+- [ ] /stores
+- [ ] /topics
+- [ ] /upload
+- [ ] /volumes
+- [ ] /yarn_attributes
+- [x] /yarn_companies
+- [ ] /yarns
+
+Models:
+
+- [ ] Activity
+- [ ] Ad
+- [ ] AttributeGroup
+- [ ] Bookmark
+- [ ] Bundle
+- [ ] BundledItem
+- [ ] Business
+- [ ] Cart
+- [ ] CartItem
+- [ ] Collection
+- [x] ColorFamily
+- [x] Colorway
+- [ ] CombinedCart
+- [ ] ComponentYarn
+- [x] Craft
+- [ ] Delivery
+- [ ] Document
+- [ ] DownloadLink
+- [ ] DraftComponentYarn
+- [ ] DraftErrataLink
+- [ ] DraftNeedleSize
+- [ ] DraftPattern
+- [ ] DraftPatternYarn
+- [x] FiberAttribute
+- [x] FiberAttributeGroup
+- [x] FiberCategory
+- [ ] FiberPack
+- [ ] FiberStash
+- [x] FiberType
+- [ ] Forum
+- [ ] ForumPost
+- [ ] ForumPreference
+- [ ] ForumSet
+- [ ] ForumStatisticSummary
+- [ ] Friendship
+- [ ] Group
+- [ ] InStoreSale
+- [ ] Invoice
+- [ ] InvoiceLineItem
+- [ ] Language
+- [ ] Message
+- [ ] NeedleRecord
+- [ ] NeedleSize
+- [ ] NeedleType
+- [ ] Pack
+- [ ] Pattern
+- [ ] PatternAttribute
+- [ ] PatternAuthor
+- [ ] PatternCategory
+- [ ] PatternClassification
+- [ ] PatternLanguage
+- [ ] PatternNeedleSize
+- [ ] PatternSource
+- [ ] PatternSourceType
+- [ ] PatternTagging
+- [x] Photo
+- [ ] Printing
+- [ ] Product
+- [ ] ProductAttachment
+- [ ] ProductNotification
+- [ ] Project
+- [ ] ProjectStatus
+- [ ] QueuedProject
+- [ ] QueuedStash
+- [ ] Saleable
+- [ ] SavedSearch
+- [ ] Shop
+- [ ] ShopCustomer
+- [ ] ShopSchedule
+- [ ] SocialSite
+- [ ] Stash
+- [ ] StashStatus
+- [ ] Store
+- [ ] Tool
+- [ ] Topic
+- [ ] UnifiedStash
+- [ ] User
+- [ ] UserSite
+- [ ] Volume
+- [ ] VolumeAttachment
+- [x] Yarn
+- [x] YarnAttributeGroup
+- [x] YarnCompany
+- [x] YarnCountry
+- [x] YarnFiber
+- [x] YarnProvenance
+- [x] YarnWeight
diff --git a/getting_started.md b/getting_started.md
new file mode 100644
index 0000000..e71eba8
--- /dev/null
+++ b/getting_started.md
@@ -0,0 +1,59 @@
+### 1. Create a New Repository
+
+First, create a repository on GitHub with the same name as this project, and then run the following commands:
+
+```bash
+git init -b main
+git add .
+git commit -m "init commit"
+git remote add origin git@github.com:theuerc/pyravelry.git
+git push -u origin main
+```
+
+### 2. Set Up Your Development Environment
+
+Then, install the environment and the pre-commit hooks with
+
+```bash
+make install
+```
+
+This will also generate your `uv.lock` file
+
+### 3. Run the pre-commit hooks
+
+Initially, the CI/CD pipeline might be failing due to formatting issues. To resolve those run:
+
+```bash
+uv run pre-commit run -a
+```
+
+### 4. Commit the changes
+
+Lastly, commit the changes made by the two steps above to your repository.
+
+```bash
+git add .
+git commit -m 'Fix formatting issues'
+git push origin main
+```
+
+You are now ready to start development on your project!
+The CI/CD pipeline will be triggered when you open a pull request, merge to main, or when you create a new release.
+
+To finalize the set-up for publishing to PyPI, see [here](https://fpgmaas.github.io/cookiecutter-uv/features/publishing/#set-up-for-pypi).
+For activating the automatic documentation with MkDocs, see [here](https://fpgmaas.github.io/cookiecutter-uv/features/mkdocs/#enabling-the-documentation-on-github).
+To enable the code coverage reports, see [here](https://fpgmaas.github.io/cookiecutter-uv/features/codecov/).
+
+## Releasing a new version
+
+- Create an API Token on [PyPI](https://pypi.org/).
+- Add the API Token to your projects secrets with the name `PYPI_TOKEN` by visiting [this page](https://github.com/theuerc/pyravelry/settings/secrets/actions/new).
+- Create a [new release](https://github.com/theuerc/pyravelry/releases/new) on Github.
+- Create a new tag in the form `*.*.*`.
+
+For more details, see [here](https://fpgmaas.github.io/cookiecutter-uv/features/cicd/#how-to-trigger-a-release).
+
+---
+
+Repository initiated with [fpgmaas/cookiecutter-uv](https://github.com/fpgmaas/cookiecutter-uv).
diff --git a/notebooks/01_getting_started.ipynb b/notebooks/01_getting_started.ipynb
index 0f77f7e..09d78f0 100644
--- a/notebooks/01_getting_started.ipynb
+++ b/notebooks/01_getting_started.ipynb
@@ -19,7 +19,7 @@
},
{
"cell_type": "code",
- "execution_count": 17,
+ "execution_count": 65,
"metadata": {},
"outputs": [],
"source": [
@@ -51,18 +51,16 @@
},
{
"cell_type": "code",
- "execution_count": 41,
+ "execution_count": 62,
"metadata": {},
"outputs": [],
"source": [
- "df = pd.DataFrame([\n",
- " {**{k: v for k, v in obj.__dict__.items() if k != \"record\"}, **obj.record.__dict__} for obj in results\n",
- "])"
+ "df = pd.json_normalize([obj.model_dump() for obj in results])"
]
},
{
"cell_type": "code",
- "execution_count": 42,
+ "execution_count": 63,
"metadata": {},
"outputs": [
{
@@ -72,17 +70,17 @@
"\n",
"RangeIndex: 50 entries, 0 to 49\n",
"Data columns (total 9 columns):\n",
- " # Column Non-Null Count Dtype \n",
- "--- ------ -------------- ----- \n",
- " 0 title 50 non-null object\n",
- " 1 type_name 50 non-null object\n",
- " 2 caption 50 non-null object\n",
- " 3 tiny_image_url 45 non-null object\n",
- " 4 image_url 45 non-null object\n",
- " 5 type 50 non-null object\n",
- " 6 id 50 non-null int64 \n",
- " 7 permalink 50 non-null object\n",
- " 8 uri 50 non-null object\n",
+ " # Column Non-Null Count Dtype \n",
+ "--- ------ -------------- ----- \n",
+ " 0 title 50 non-null object\n",
+ " 1 type_name 50 non-null object\n",
+ " 2 caption 50 non-null object\n",
+ " 3 tiny_image_url 45 non-null object\n",
+ " 4 image_url 45 non-null object\n",
+ " 5 record.type 50 non-null object\n",
+ " 6 record.id 50 non-null int64 \n",
+ " 7 record.permalink 50 non-null object\n",
+ " 8 record.uri 50 non-null object\n",
"dtypes: int64(1), object(8)\n",
"memory usage: 3.6+ KB\n"
]
@@ -94,7 +92,7 @@
},
{
"cell_type": "code",
- "execution_count": 43,
+ "execution_count": 64,
"metadata": {},
"outputs": [
{
@@ -155,7 +153,7 @@
"3 Merino-Tussah yarn"
]
},
- "execution_count": 43,
+ "execution_count": 64,
"metadata": {},
"output_type": "execute_result"
}
diff --git a/src/pyravelry/client.py b/src/pyravelry/client.py
index b3c3b5a..f55c7c0 100644
--- a/src/pyravelry/client.py
+++ b/src/pyravelry/client.py
@@ -10,6 +10,7 @@
FiberAttributesResource,
FiberCategoriesResource,
SearchResource,
+ YarnCompaniesResource,
YarnWeightsResource,
)
@@ -37,6 +38,7 @@ def __init__(self, settings: RavelrySettings) -> None:
self.yarn_weights = YarnWeightsResource(self._http)
self.search = SearchResource(self._http)
self.fiber_attributes = FiberAttributesResource(self._http)
+ self.yarn_companies = YarnCompaniesResource(self._http)
def close(self) -> None:
"""Closes the httpx client."""
diff --git a/src/pyravelry/endpoints/__init__.py b/src/pyravelry/endpoints/__init__.py
index 0ec1e70..11f0cc6 100644
--- a/src/pyravelry/endpoints/__init__.py
+++ b/src/pyravelry/endpoints/__init__.py
@@ -5,6 +5,7 @@
from .fiber_attributes import FiberAttributesResource
from .fiber_categories import FiberCategoriesResource
from .search import SearchResource
+from .yarn_companies import YarnCompaniesResource
from .yarn_weights import YarnWeightsResource
__all__ = [
@@ -12,6 +13,7 @@
"FiberAttributesResource",
"FiberCategoriesResource",
"SearchResource",
+ "YarnCompaniesResource",
"YarnWeightsResource",
"base",
]
diff --git a/src/pyravelry/endpoints/base.py b/src/pyravelry/endpoints/base.py
index 5fa1c94..11e9e99 100644
--- a/src/pyravelry/endpoints/base.py
+++ b/src/pyravelry/endpoints/base.py
@@ -15,6 +15,12 @@ def endpoint(self) -> str:
"""Each child must define this string (e.g., '/patterns')."""
pass
+ @property
+ @abstractmethod
+ def output_model(self) -> Any:
+ """Each child must define this output pydantic model"""
+ pass
+
def __init__(self, http_client: SyncCacheClient) -> None:
"""Initializes the base endpoint for Ravelry.
diff --git a/src/pyravelry/endpoints/color_families.py b/src/pyravelry/endpoints/color_families.py
index 98e6b34..ab734cc 100644
--- a/src/pyravelry/endpoints/color_families.py
+++ b/src/pyravelry/endpoints/color_families.py
@@ -31,6 +31,7 @@ def list(self) -> list[ColorFamilyModel]:
Defined at:
https://www.ravelry.com/api#/_color_families
"""
- response_dict = self._fetch(http_client=self._http, endpoint=ColorFamiliesResource.endpoint)
- data = ColorFamiliesModel.model_validate(response_dict)
+ cls = ColorFamiliesResource
+ response_dict = self._fetch(http_client=self._http, endpoint=cls.endpoint)
+ data = cls.output_model.model_validate(response_dict)
return data.color_families
diff --git a/src/pyravelry/endpoints/fiber_attributes.py b/src/pyravelry/endpoints/fiber_attributes.py
index 25d100e..2e3ec9d 100644
--- a/src/pyravelry/endpoints/fiber_attributes.py
+++ b/src/pyravelry/endpoints/fiber_attributes.py
@@ -25,6 +25,7 @@ def list(self) -> list[FiberAttributeModel]:
List the current fiber attributes
Endpoint: GET /fiber_attributes.json
"""
- response_dict = self._fetch(http_client=self._http, endpoint=FiberAttributesResource.endpoint)
- data = FiberAttributesModel.model_validate(response_dict)
+ cls = FiberAttributesResource
+ response_dict = self._fetch(http_client=self._http, endpoint=cls.endpoint)
+ data = cls.output_model.model_validate(response_dict)
return data.fiber_attributes
diff --git a/src/pyravelry/endpoints/fiber_categories.py b/src/pyravelry/endpoints/fiber_categories.py
index 8639ea0..5b516e7 100644
--- a/src/pyravelry/endpoints/fiber_categories.py
+++ b/src/pyravelry/endpoints/fiber_categories.py
@@ -25,6 +25,7 @@ def list(self) -> list[FiberCategoryModel]:
List the current fiber categories
Endpoint: GET /fiber_categories.json
"""
- response_dict = self._fetch(http_client=self._http, endpoint=FiberCategoriesResource.endpoint)
- data = FiberCategoriesModel.model_validate(response_dict)
+ cls = FiberCategoriesResource
+ response_dict = self._fetch(http_client=self._http, endpoint=cls.endpoint)
+ data = cls.output_model.model_validate(response_dict)
return data.fiber_categories
diff --git a/src/pyravelry/endpoints/search.py b/src/pyravelry/endpoints/search.py
index 975c6da..a56588e 100644
--- a/src/pyravelry/endpoints/search.py
+++ b/src/pyravelry/endpoints/search.py
@@ -54,7 +54,8 @@ def query(
Usage:
search.query(query="merino", limit=10, types=["Yarn"])
"""
- params_obj = SearchParams(query=query, limit=limit, types=types)
+ cls = SearchResource
+ params_obj = cls.input_model(query=query, limit=limit, types=types)
# Flatten the 'types' list into a space-delimited string
params_dict = params_obj.model_dump(exclude_none=True)
@@ -63,9 +64,9 @@ def query(
response_dict = self._fetch(
http_client=self._http,
- endpoint=self.endpoint,
+ endpoint=cls.endpoint,
params=params_dict,
)
- data = GlobalSearchResponseModel.model_validate(response_dict)
+ data = cls.output_model.model_validate(response_dict)
return data.results
diff --git a/src/pyravelry/endpoints/yarn_companies.py b/src/pyravelry/endpoints/yarn_companies.py
new file mode 100644
index 0000000..2fcd860
--- /dev/null
+++ b/src/pyravelry/endpoints/yarn_companies.py
@@ -0,0 +1,47 @@
+"""Endpoint for yarn companies"""
+
+from typing import Optional
+
+from pyravelry.endpoints.base import BaseEndpoint
+from pyravelry.models import YarnCompanySearchParams, YarnCompanySearchResponseModel
+
+
+class YarnCompaniesResource(BaseEndpoint):
+ """
+ Endpoint for yarn company specific operations.
+ https://www.ravelry.com/api#yarn_companies_search
+ """
+
+ endpoint = "/yarn_companies"
+ input_model = YarnCompanySearchParams
+ output_model = YarnCompanySearchResponseModel
+
+ def query(
+ self,
+ query: Optional[str] = None,
+ page: int = 1,
+ page_size: int = 48,
+ sort: str = "best",
+ ) -> YarnCompanySearchResponseModel:
+ """
+ Search the yarn company directory.
+
+ Args:
+ query: Search term for fulltext searching.
+ page: Result page to retrieve.
+ page_size: Number of results per page.
+ sort: Sort order (e.g., 'best', 'best_'; reverse order with _ suffix)
+ """
+ cls = YarnCompaniesResource
+
+ url = "/".join([cls.endpoint, "/search.json"])
+
+ params = cls.input_model(query=query, page=page, page_size=page_size, sort=sort)
+
+ response_dict = self._fetch(
+ http_client=self._http,
+ endpoint=url,
+ params=params.model_dump(exclude_none=True),
+ )
+
+ return cls.output_model.model_validate(response_dict)
diff --git a/src/pyravelry/endpoints/yarn_weights.py b/src/pyravelry/endpoints/yarn_weights.py
index 9665ec0..e6f247f 100644
--- a/src/pyravelry/endpoints/yarn_weights.py
+++ b/src/pyravelry/endpoints/yarn_weights.py
@@ -1,7 +1,4 @@
-"""Endpoint for yarn weights.
-
-https://www.ravelry.com/api#/_yarn_weights
-"""
+"""Endpoint for yarn weights."""
from pyravelry.endpoints.base import BaseEndpoint
from pyravelry.models import YarnWeightModel, YarnWeightsModel
@@ -15,6 +12,8 @@ class YarnWeightsResource(BaseEndpoint):
Methods:
list (list[YarnWeightModel]): returns all yarn weights.
+
+ https://www.ravelry.com/api#/_yarn_weights
"""
endpoint: str = "/yarn_weights.json"
@@ -25,6 +24,7 @@ def list(self) -> list[YarnWeightModel]:
List the current yarn weights.
Endpoint: GET /yarn_weights.json
"""
- response_dict = self._fetch(http_client=self._http, endpoint=YarnWeightsResource.endpoint)
- data = YarnWeightsModel.model_validate(response_dict)
+ cls = YarnWeightsResource
+ response_dict = self._fetch(http_client=self._http, endpoint=cls.endpoint)
+ data = cls.output_model.model_validate(response_dict)
return data.yarn_weights
diff --git a/src/pyravelry/models/__init__.py b/src/pyravelry/models/__init__.py
index 8181f87..7cd6026 100644
--- a/src/pyravelry/models/__init__.py
+++ b/src/pyravelry/models/__init__.py
@@ -1,14 +1,23 @@
+"""Results Objects that are used in endpoints"""
+
from . import base
from .base import BaseRavelryModel
from .colorfamily import ColorFamiliesModel, ColorFamilyModel
-from .fiberattribute import FiberAttributeModel, FiberAttributesModel
-from .fibercategory import FiberCategoriesModel, FiberCategoryModel
-from .user_input_models.search import SearchParams
-from .user_input_models.searchresult import (
+from .custom_models import paginator
+from .custom_models.search import (
GlobalSearchResponseModel,
+ SearchParams,
SearchRecordModel,
SearchResultModel,
)
+from .custom_models.yarncompany import (
+ YarnCompanyModel,
+ YarnCompanySearchParams,
+ YarnCompanySearchResponseModel,
+)
+from .fiberattribute import FiberAttributeModel, FiberAttributesModel
+from .fibercategory import FiberCategoriesModel, FiberCategoryModel
+from .yarncompany import YarnCompaniesModel
from .yarnweight import YarnWeightModel, YarnWeightsModel
__all__ = [
@@ -23,7 +32,12 @@
"SearchParams",
"SearchRecordModel",
"SearchResultModel",
+ "YarnCompaniesModel",
+ "YarnCompanyModel",
+ "YarnCompanySearchParams",
+ "YarnCompanySearchResponseModel",
"YarnWeightModel",
"YarnWeightsModel",
"base",
+ "paginator",
]
diff --git a/src/pyravelry/models/custom_models/__init__.py b/src/pyravelry/models/custom_models/__init__.py
new file mode 100644
index 0000000..02e2c2e
--- /dev/null
+++ b/src/pyravelry/models/custom_models/__init__.py
@@ -0,0 +1,8 @@
+"""Custom models that are required for some endpoints.
+
+These are not defined explicitly in the Ravelry documentation.
+"""
+
+from . import paginator, search, yarncompany
+
+__all__ = ["paginator", "search", "yarncompany"]
diff --git a/src/pyravelry/models/custom_models/paginator.py b/src/pyravelry/models/custom_models/paginator.py
new file mode 100644
index 0000000..cc22d4d
--- /dev/null
+++ b/src/pyravelry/models/custom_models/paginator.py
@@ -0,0 +1,17 @@
+"""Paginator model"""
+
+from pyravelry.models.base import BaseRavelryModel
+
+
+class PaginatorModel(BaseRavelryModel):
+ """Standard Ravelry pagination object.
+
+ Documentation a little above this url:
+ https://www.ravelry.com/api#/_color_families
+ """
+
+ page_count: int
+ page: int
+ page_size: int
+ results: int
+ last_page: int
diff --git a/src/pyravelry/models/user_input_models/search.py b/src/pyravelry/models/custom_models/search.py
similarity index 57%
rename from src/pyravelry/models/user_input_models/search.py
rename to src/pyravelry/models/custom_models/search.py
index 6a0f46b..27eeb25 100644
--- a/src/pyravelry/models/user_input_models/search.py
+++ b/src/pyravelry/models/custom_models/search.py
@@ -1,15 +1,17 @@
-"""Search parameter models
-
-https://www.ravelry.com/api#/_search
-"""
+"""Search parameter models"""
from typing import Literal, Optional
from pydantic import BaseModel, Field, field_validator
+from pyravelry.models.base import BaseRavelryModel
+
class SearchParams(BaseModel):
- """Parameters for the /search.json endpoint."""
+ """Parameters for the /search.json endpoint.
+
+ https://www.ravelry.com/api#/_search
+ """
query: str = Field(...)
limit: int = Field(50, ge=1, le=500)
@@ -49,3 +51,29 @@ def format_types(cls, v: str) -> list[str]:
if isinstance(v, str):
return v.strip().split(" ")
return v
+
+
+class SearchRecordModel(BaseRavelryModel):
+ """Details about the specific record found in the search."""
+
+ type: str
+ id: int
+ permalink: str
+ uri: Optional[str] = None
+
+
+class SearchResultModel(BaseRavelryModel):
+ """Represents an individual result from the global search."""
+
+ title: str
+ type_name: str = Field(..., alias="type_name")
+ caption: Optional[str] = None
+ tiny_image_url: Optional[str] = None
+ image_url: Optional[str] = None
+ record: SearchRecordModel
+
+
+class GlobalSearchResponseModel(BaseRavelryModel):
+ """Wrapper for the search.json response."""
+
+ results: list[SearchResultModel]
diff --git a/src/pyravelry/models/custom_models/yarncompany.py b/src/pyravelry/models/custom_models/yarncompany.py
new file mode 100644
index 0000000..44c823b
--- /dev/null
+++ b/src/pyravelry/models/custom_models/yarncompany.py
@@ -0,0 +1,22 @@
+from typing import Optional
+
+from pyravelry.models.base import BaseRavelryModel
+from pyravelry.models.yarncompany import YarnCompanyModel
+
+from .paginator import PaginatorModel
+
+
+class YarnCompanySearchParams(BaseRavelryModel):
+ """Parameters for the yarn_companies/search endpoint."""
+
+ query: Optional[str] = None
+ page: int = 1
+ page_size: int = 48
+ sort: Optional[str] = "best"
+
+
+class YarnCompanySearchResponseModel(BaseRavelryModel):
+ """Response returned by /yarn_companies/search.json."""
+
+ yarn_companies: list[YarnCompanyModel]
+ paginator: PaginatorModel
diff --git a/src/pyravelry/models/user_input_models/__init__.py b/src/pyravelry/models/user_input_models/__init__.py
deleted file mode 100644
index 1b6542e..0000000
--- a/src/pyravelry/models/user_input_models/__init__.py
+++ /dev/null
@@ -1,6 +0,0 @@
-from . import search, searchresult
-
-__all__ = [
- "search",
- "searchresult",
-]
diff --git a/src/pyravelry/models/user_input_models/searchresult.py b/src/pyravelry/models/user_input_models/searchresult.py
deleted file mode 100644
index 12c5d33..0000000
--- a/src/pyravelry/models/user_input_models/searchresult.py
+++ /dev/null
@@ -1,36 +0,0 @@
-"""Search results models
-
-https://www.ravelry.com/api#/_search
-"""
-
-from typing import Optional
-
-from pydantic import Field
-
-from pyravelry.models.base import BaseRavelryModel
-
-
-class SearchRecordModel(BaseRavelryModel):
- """Details about the specific record found in the search."""
-
- type: str
- id: int
- permalink: str
- uri: Optional[str] = None
-
-
-class SearchResultModel(BaseRavelryModel):
- """Represents an individual result from the global search."""
-
- title: str
- type_name: str = Field(..., alias="type_name")
- caption: Optional[str] = None
- tiny_image_url: Optional[str] = None
- image_url: Optional[str] = None
- record: SearchRecordModel
-
-
-class GlobalSearchResponseModel(BaseRavelryModel):
- """Wrapper for the search.json response."""
-
- results: list[SearchResultModel]
diff --git a/tests/client_test.py b/tests/client_test.py
index 3a97ae0..b319575 100644
--- a/tests/client_test.py
+++ b/tests/client_test.py
@@ -23,6 +23,7 @@ def test_initialization(self) -> None:
"yarn_weights",
"search",
"fiber_attributes",
+ "yarn_companies",
],
)
def test_attributes__with_context_manager(self, attribute: str) -> None:
diff --git a/tests/endpoints_test/cassettes/yarn_companies_test/TestYarnCompanyResources.test_query.yaml b/tests/endpoints_test/cassettes/yarn_companies_test/TestYarnCompanyResources.test_query.yaml
new file mode 100644
index 0000000..448f561
--- /dev/null
+++ b/tests/endpoints_test/cassettes/yarn_companies_test/TestYarnCompanyResources.test_query.yaml
@@ -0,0 +1,73 @@
+interactions:
+- request:
+ body: ''
+ headers:
+ Accept:
+ - '*/*'
+ Accept-Encoding:
+ - gzip, deflate
+ Connection:
+ - keep-alive
+ Host:
+ - api.ravelry.com
+ User-Agent:
+ - python-httpx/0.28.1
+ authorization:
+ - Basic
+ method: GET
+ uri: https://api.ravelry.com/yarn_companies//search.json?query=Lion&page=1&page_size=10&sort=best_
+ response:
+ body:
+ string: '{"yarn_companies": [{"id":99,"name":"Lion Brand","permalink":"lion-brand","url":"http://www.lionbrand.com","yarns_count":489},{"id":28146,"name":"Lion
+ Color","permalink":"lion-color","url":"","yarns_count":1},{"id":16901,"name":"The
+ Woolly Lion","permalink":"the-woolly-lion","url":null,"yarns_count":1},{"id":28157,"name":"Ferme
+ du Lion Vert","permalink":"ferme-du-lion-vert","url":null,"yarns_count":1},{"id":14131,"name":"Cottage
+ Folk Yarns","permalink":"cottage-folk-yarns","url":"","yarns_count":11},{"id":305,"name":"Silk
+ City Fibers","permalink":"silk-city-fibers","url":"http://www.silkcityfibers.com/","yarns_count":76}],
+ "paginator": {"page_count":1,"page":1,"page_size":10,"results":6,"last_page":1}}'
+ headers:
+ Access-Control-Allow-Credentials:
+ - 'true'
+ Access-Control-Allow-Headers:
+ - Authorization,Accept,Origin,DNT,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range
+ Access-Control-Allow-Methods:
+ - GET,POST,OPTIONS,PUT,DELETE,PATCH
+ Cache-Control:
+ - private, max-age=0, must-revalidate
+ Connection:
+ - keep-alive
+ Content-Type:
+ - application/json; charset=utf-8
+ Date:
+ - Sun, 28 Dec 2025 18:10:38 GMT
+ ETag:
+ - W/"c6e66c7cc4eeadd3f03689aaadae6d5e"
+ Server:
+ - nginx
+ Status:
+ - 200 OK
+ Transfer-Encoding:
+ - chunked
+ Vary:
+ - Accept-Encoding
+ - Origin
+ X-API:
+ - '1'
+ X-Frame-Options:
+ - SAMEORIGIN
+ X-Handled-By:
+ - yearling
+ X-Ruby:
+ - '2'
+ X-Runtime:
+ - '31'
+ X-Up-Location:
+ - https://api.ravelry.com/yarn_companies/search.json?query=Lion&page=1&page_size=10&sort=best_
+ X-Up-Method:
+ - get
+ content-length:
+ - '711'
+ status:
+ code: 200
+ message: OK
+version: 1
diff --git a/tests/endpoints_test/cassettes/yarn_company_test/TestYarnCompanyResources.test_query.yaml b/tests/endpoints_test/cassettes/yarn_company_test/TestYarnCompanyResources.test_query.yaml
new file mode 100644
index 0000000..5420ff2
--- /dev/null
+++ b/tests/endpoints_test/cassettes/yarn_company_test/TestYarnCompanyResources.test_query.yaml
@@ -0,0 +1,73 @@
+interactions:
+- request:
+ body: ''
+ headers:
+ Accept:
+ - '*/*'
+ Accept-Encoding:
+ - gzip, deflate
+ Connection:
+ - keep-alive
+ Host:
+ - api.ravelry.com
+ User-Agent:
+ - python-httpx/0.28.1
+ authorization:
+ - Basic
+ method: GET
+ uri: https://api.ravelry.com/yarn_companies//search.json?query=Lion&page=1&page_size=10&sort=best_
+ response:
+ body:
+ string: '{"yarn_companies": [{"id":99,"name":"Lion Brand","permalink":"lion-brand","url":"http://www.lionbrand.com","yarns_count":489},{"id":28146,"name":"Lion
+ Color","permalink":"lion-color","url":"","yarns_count":1},{"id":16901,"name":"The
+ Woolly Lion","permalink":"the-woolly-lion","url":null,"yarns_count":1},{"id":28157,"name":"Ferme
+ du Lion Vert","permalink":"ferme-du-lion-vert","url":null,"yarns_count":1},{"id":14131,"name":"Cottage
+ Folk Yarns","permalink":"cottage-folk-yarns","url":"","yarns_count":11},{"id":305,"name":"Silk
+ City Fibers","permalink":"silk-city-fibers","url":"http://www.silkcityfibers.com/","yarns_count":76}],
+ "paginator": {"page_count":1,"page":1,"page_size":10,"results":6,"last_page":1}}'
+ headers:
+ Access-Control-Allow-Credentials:
+ - 'true'
+ Access-Control-Allow-Headers:
+ - Authorization,Accept,Origin,DNT,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range
+ Access-Control-Allow-Methods:
+ - GET,POST,OPTIONS,PUT,DELETE,PATCH
+ Cache-Control:
+ - private, max-age=0, must-revalidate
+ Connection:
+ - keep-alive
+ Content-Type:
+ - application/json; charset=utf-8
+ Date:
+ - Sun, 28 Dec 2025 17:58:37 GMT
+ ETag:
+ - W/"c6e66c7cc4eeadd3f03689aaadae6d5e"
+ Server:
+ - nginx
+ Status:
+ - 200 OK
+ Transfer-Encoding:
+ - chunked
+ Vary:
+ - Accept-Encoding
+ - Origin
+ X-API:
+ - '1'
+ X-Frame-Options:
+ - SAMEORIGIN
+ X-Handled-By:
+ - yearling
+ X-Ruby:
+ - '2'
+ X-Runtime:
+ - '18'
+ X-Up-Location:
+ - https://api.ravelry.com/yarn_companies/search.json?query=Lion&page=1&page_size=10&sort=best_
+ X-Up-Method:
+ - get
+ content-length:
+ - '711'
+ status:
+ code: 200
+ message: OK
+version: 1
diff --git a/tests/endpoints_test/yarn_companies_test.py b/tests/endpoints_test/yarn_companies_test.py
new file mode 100644
index 0000000..1c5651a
--- /dev/null
+++ b/tests/endpoints_test/yarn_companies_test.py
@@ -0,0 +1,28 @@
+from typing import Any
+
+import pytest
+
+from pyravelry.endpoints import YarnCompaniesResource
+from pyravelry.models import YarnCompanyModel, YarnCompanySearchResponseModel
+
+
+@pytest.mark.vcr
+class TestYarnCompanyResources:
+ @pytest.fixture(autouse=True)
+ def setup(self, api_info: Any) -> None:
+ """Automatically sets up the resource for every test in this class."""
+ self.obj = YarnCompaniesResource(api_info["client"])
+
+ def test_initialization(self) -> None:
+ assert self.obj is not None
+
+ def test_query(self) -> None:
+ results = self.obj.query(
+ query="Lion",
+ page=1,
+ page_size=10,
+ sort="best_",
+ )
+ assert isinstance(results, YarnCompanySearchResponseModel)
+ assert len(results.yarn_companies) > 0
+ assert isinstance(results.yarn_companies[0], YarnCompanyModel)