diff --git a/README.md b/README.md index 3cd0a9f..d935874 100644 --- a/README.md +++ b/README.md @@ -6,186 +6,25 @@ [![Commit activity](https://img.shields.io/github/commit-activity/m/theuerc/pyravelry)](https://img.shields.io/github/commit-activity/m/theuerc/pyravelry) [![License](https://img.shields.io/github/license/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 @@ [![Commit activity](https://img.shields.io/github/commit-activity/m/theuerc/pyravelry)](https://img.shields.io/github/commit-activity/m/theuerc/pyravelry) [![License](https://img.shields.io/github/license/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)