diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index f8ec38c..620d46d 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -82,6 +82,10 @@ - __all__ lists must contain only strings, no unpacking or concatenation (PLE0604, PLE0605) - Catch specific exceptions instead of broad Exception classes (BLE001, S110) - For functions with many parameters (>5), use `# noqa: PLR0913` comment when needed for convenience functions +- For F401 unused imports in type stubs (under if False), add to __all__ +- For N818 exception names, add Error suffix +- For PLR0911 too many return statements, use dict mapping or match to reduce returns +- For SLF001 private member access in tests (e.g., _client), add noqa if necessary for mocking ## Testing diff --git a/.github/openapi-config.yml b/.github/openapi-config.yml new file mode 100644 index 0000000..ff82230 --- /dev/null +++ b/.github/openapi-config.yml @@ -0,0 +1,12 @@ +project_name_override: templafy +package_name_override: templafy +literal_enums: true +generate_all_tags: true +post_hooks: + - "ruff check src/templafy --fix" + - "ruff format src/templafy" +field_prefix: attr_ +content_type_overrides: + application/vnd.openxmlformats-officedocument.wordprocessingml.document: application/octet-stream + application/vnd.openxmlformats-officedocument.spreadsheetml.sheet: application/octet-stream + application/vnd.openxmlformats-officedocument.presentationml.presentation: application/octet-stream \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index b9a3b4b..6def068 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,3 +1,24 @@ +# Agent Guidelines + +You are an agent - please keep going until the user’s query is completely resolved, before ending your turn and yielding back to the user. + +Your thinking should be thorough and so it's fine if it's very long. However, avoid unnecessary repetition and verbosity. You should be concise, but thorough. + +You MUST iterate and keep going until the problem is solved. + +You have everything you need to resolve this problem. I want you to fully solve this autonomously before coming back to me. + +Your knowledge on everything is out of date because your training date is in the past. + +Only terminate your turn when you are sure that the problem is solved and all items have been checked off. Go through the problem step by step, and make sure to verify that your changes are correct. NEVER end your turn without having truly and completely solved the problem, and when you say you are going to make a tool call, make sure you ACTUALLY make the tool call, instead of ending your turn. + +THE PROBLEM CAN NOT BE SOLVED WITHOUT EXTENSIVE INTERNET RESEARCH. + +You must use the context7 mcp tool and fetch_webpage tool to recursively gather all information from URL's provided to you by the user, as well as any links you find in the content of those pages. + +You MUST plan extensively before each function call, and reflect extensively on the outcomes of the previous function calls. DO NOT do this entire process by making function calls only, as this can impair your ability to solve the problem and think insightfully. + + # CI: pass pre-commit gate before committing ## One-time setup diff --git a/README.md b/README.md index 5e556a2..2eba909 100644 --- a/README.md +++ b/README.md @@ -1,58 +1,145 @@ -# Python PyPI Package Template +# Templafy API Client -https://api.templafy.com/ +A Python client for the Templafy API using openapi-python-client for type-safe, modern Python API access. -A minimal template for creating Python packages with a simple "Hello World" example. +## Overview + +This package provides a Python client for the Templafy API, allowing you to programmatically access templates, documents, images, and other assets in your Templafy workspace. + +## Installation + +```bash +pip install -e . +``` ## Quick Start -### 1. Use this template +### Basic Usage + +```python +from templafy import AuthenticatedClient +from templafy.api import spaces, documents + +# Initialize client +client = AuthenticatedClient( + base_url="https://your-tenant.api.templafy.com/v3", + token="your-api-token" +) + +# Use with context manager (recommended) +with client as client: + # List all spaces + all_spaces = spaces.get_spaces(client=client) + print(f"Found {len(all_spaces)} spaces") + + # List documents + all_documents = documents.get_documents(client=client) + print(f"Found {len(all_documents)} documents") +``` -Click the "Use this template" button on GitHub to create your own repository: +### Available API Endpoints -### 2. Install dependencies +The client provides access to the following Templafy API resources: -```bash -# Run the development setup script -.\tasks\dev_sync.ps1 +- **Spaces** - Workspace/tenant management +- **Libraries** - Library management across spaces +- **Documents** - Document template operations and generation +- **Folders** - Folder structure management +- **Images** - Image asset management +- **Slides** - PowerPoint slide management +- **Spreadsheets** - Excel template operations +- **Links** - Link asset management + +### Models + +The client includes type-safe models for all API resources: + +```python +from templafy import Space, Document, Library, Image + +# Models are automatically used when calling API methods +spaces = spaces.get_spaces(client=client) +for space in spaces: + print(f"Space: {space.name} (ID: {space.id})") ``` -### 3. Customize the package +## API Structure -#### Rename the package +``` +templafy/ +├── client.py # Base Client and AuthenticatedClient classes +├── models/ # Type-safe models for all schemas +│ ├── space.py # Space-related models +│ ├── document.py # Document-related models +│ ├── library.py # Library-related models +│ └── ... +├── api/ # API endpoint modules by resource +│ ├── spaces.py # Spaces API endpoints +│ ├── documents.py # Documents API endpoints +│ ├── libraries.py # Libraries API endpoints +│ └── ... +├── types.py # Common type definitions +└── errors.py # Error classes and exceptions +``` -1. Rename directories: - ```bash - mv src/whitelabel src/YOUR_PACKAGE_NAME - mv tests/whitelabel tests/YOUR_PACKAGE_NAME - ``` +## Error Handling + +The client provides specific error classes for different types of API errors: + +```python +from templafy.errors import ( + AuthenticationError, + AuthorizationError, + NotFoundError, + ValidationError, + RateLimitError, + ServerError +) + +try: + documents = documents.get_documents(client=client) +except AuthenticationError: + print("API token is invalid") +except AuthorizationError: + print("Insufficient permissions") +except NotFoundError: + print("Resource not found") +except RateLimitError: + print("Rate limit exceeded") +``` -2. Update `pyproject.toml`: - - Change `name = "whitelabel"` to `name = "YOUR_PACKAGE_NAME"` - - Update author information - - Update repository URLs +## Development -3. Update import statements in your code from `whitelabel` to `YOUR_PACKAGE_NAME` +### Dependencies +This project requires Python 3.12+ and the following dependencies: -## Next Steps +- `httpx` - HTTP client +- `pydantic` - Data validation and settings management +- `typing-extensions` - Additional typing features -1. Replace the hello_world function with your own code -2. Add your functions to `src/YOUR_PACKAGE_NAME/functions/` -3. Write tests in `tests/YOUR_PACKAGE_NAME/` -4. Update `pyproject.toml` with your package details +### Testing -## CI/CD Configuration +```bash +python -m pytest tests/ -v +``` + +### Code Quality -### SonarQube Setup +The project uses `ruff` for linting and formatting: -To enable SonarQube analysis in your CI/CD pipeline, set the following variables: +```bash +ruff check src/templafy --fix +ruff format src/templafy +``` -- `SONAR_TOKEN`: Set as a **secret** in your CI/CD platform (authentication token for SonarQube) -- `SONAR_PROJECT_KEY`: Set as an **environment variable** in your CI/CD pipeline (unique key for your project, e.g., `your-org_your-repo`) +## Contributing -`SONAR_HOST_URL` is typically configured at the organization level. +1. Install development dependencies +2. Make your changes +3. Run tests and linting +4. Submit a pull request ## License -MIT License +MIT License - see LICENSE file for details. diff --git a/assets/openapi.json b/assets/openapi.json index 977f5f6..19b27c8 100644 --- a/assets/openapi.json +++ b/assets/openapi.json @@ -298,7 +298,7 @@ }, "Name": { "type": "string", - "description": "The name is inferred from the file name by default. It can be overriden by providing a different value with this field" + "description": "The name is inferred from the file name by default. It can be overridden by providing a different value with this field" }, "Description": { "type": "string", @@ -810,7 +810,7 @@ }, "Name": { "type": "string", - "description": "The name is inferred from the file name by default. It can be overriden by providing a different value with this field" + "description": "The name is inferred from the file name by default. It can be overridden by providing a different value with this field" }, "Description": { "type": "string", @@ -1776,7 +1776,7 @@ }, "Name": { "type": "string", - "description": "The name is inferred from the file name by default. It can be overriden by providing a different value with this field" + "description": "The name is inferred from the file name by default. It can be overridden by providing a different value with this field" }, "Description": { "type": "string", @@ -2838,7 +2838,7 @@ }, "Name": { "type": "string", - "description": "The name is inferred from the file name by default. It can be overriden by providing a different value with this field" + "description": "The name is inferred from the file name by default. It can be overridden by providing a different value with this field" }, "Description": { "type": "string", @@ -3439,7 +3439,7 @@ }, "Name": { "type": "string", - "description": "The name is inferred from the file name by default. It can be overriden by providing a different value with this field" + "description": "The name is inferred from the file name by default. It can be overridden by providing a different value with this field" }, "Description": { "type": "string", @@ -3973,7 +3973,7 @@ }, "Name": { "type": "string", - "description": "The name is inferred from the file name by default. It can be overriden by providing a different value with this field" + "description": "The name is inferred from the file name by default. It can be overridden by providing a different value with this field" }, "Description": { "type": "string", @@ -4507,7 +4507,7 @@ }, "Name": { "type": "string", - "description": "The name is inferred from the file name by default. It can be overriden by providing a different value with this field" + "description": "The name is inferred from the file name by default. It can be overridden by providing a different value with this field" }, "Description": { "type": "string", @@ -5119,7 +5119,7 @@ }, "Name": { "type": "string", - "description": "The name is inferred from the file name by default. It can be overriden by providing a different value with this field" + "description": "The name is inferred from the file name by default. It can be overridden by providing a different value with this field" }, "Description": { "type": "string", @@ -5713,7 +5713,7 @@ }, "Name": { "type": "string", - "description": "The name is inferred from the file name by default. It can be overriden by providing a different value with this field" + "description": "The name is inferred from the file name by default. It can be overridden by providing a different value with this field" }, "Description": { "type": "string", diff --git a/pyproject.toml b/pyproject.toml index e853e02..73ef522 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,8 +12,8 @@ requires = [ ] [project] -name = "whitelabel" -description = "A minimal Python package template with a simple Hello World example." +name = "templafy" +description = "A Python client for the Templafy API using openapi-python-client." readme = "README.md" license = "MIT" license-files = [ "LICENSE" ] @@ -33,7 +33,10 @@ classifiers = [ "Programming Language :: Python :: 3.13", ] dynamic = [ "urls", "version" ] -dependencies = [ ] +dependencies = [ + "httpx>=0.24.0", + "pydantic>=2.0.0", +] [dependency-groups] dev = [ @@ -44,6 +47,7 @@ dev = [ "ipywidgets>=8.1.7", "matplotlib>=3.10.3", "nbstripout>=0.8.1", + "openapi-python-client>=0.15.0", "perfplot>=0.10.2", "pip>=25.1.1", "pre-commit>=4.2.0", @@ -74,7 +78,7 @@ source = "vcs" "Source Archive" = "https://github.com/tonkintaylor/YOUR_REPOSITORY/archive/{commit_hash}.zip" [tool.hatch.build.hooks.vcs] -version-file = "src/whitelabel/_version.py" +version-file = "src/templafy/_version.py" [tool.ruff] line-length = 88 diff --git a/requirements.txt b/requirements.txt index 6902737..92a0ec6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,139 @@ # This file was autogenerated by uv via the following command: # uv export --frozen --offline --no-default-groups -o=requirements.txt -e . +annotated-types==0.7.0 \ + --hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 \ + --hash=sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89 + # via pydantic +anyio==4.10.0 \ + --hash=sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6 \ + --hash=sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1 + # via httpx +certifi==2025.7.14 \ + --hash=sha256:6b31f564a415d79ee77df69d757bb49a5bb53bd9f756cbbe24394ffd6fc1f4b2 \ + --hash=sha256:8ea99dbdfaaf2ba2f9bac77b9249ef62ec5218e7c2b2e903378ed5fccf765995 + # via + # httpcore + # httpx +exceptiongroup==1.3.0 ; python_full_version < '3.11' \ + --hash=sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10 \ + --hash=sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88 + # via anyio +h11==0.16.0 \ + --hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \ + --hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86 + # via httpcore +httpcore==1.0.9 \ + --hash=sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 \ + --hash=sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8 + # via httpx +httpx==0.28.1 \ + --hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \ + --hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad + # via templafy +idna==3.10 \ + --hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \ + --hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3 + # via + # anyio + # httpx +pydantic==2.11.7 \ + --hash=sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db \ + --hash=sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b + # via templafy +pydantic-core==2.33.2 \ + --hash=sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d \ + --hash=sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac \ + --hash=sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02 \ + --hash=sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56 \ + --hash=sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22 \ + --hash=sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef \ + --hash=sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec \ + --hash=sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d \ + --hash=sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a \ + --hash=sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f \ + --hash=sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052 \ + --hash=sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab \ + --hash=sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916 \ + --hash=sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c \ + --hash=sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf \ + --hash=sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a \ + --hash=sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8 \ + --hash=sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7 \ + --hash=sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612 \ + --hash=sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1 \ + --hash=sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7 \ + --hash=sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a \ + --hash=sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b \ + --hash=sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7 \ + --hash=sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025 \ + --hash=sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849 \ + --hash=sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b \ + --hash=sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa \ + --hash=sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e \ + --hash=sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea \ + --hash=sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac \ + --hash=sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51 \ + --hash=sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e \ + --hash=sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162 \ + --hash=sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65 \ + --hash=sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2 \ + --hash=sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b \ + --hash=sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de \ + --hash=sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc \ + --hash=sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb \ + --hash=sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d \ + --hash=sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef \ + --hash=sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1 \ + --hash=sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5 \ + --hash=sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88 \ + --hash=sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290 \ + --hash=sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d \ + --hash=sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808 \ + --hash=sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc \ + --hash=sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc \ + --hash=sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e \ + --hash=sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640 \ + --hash=sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30 \ + --hash=sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e \ + --hash=sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9 \ + --hash=sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9 \ + --hash=sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f \ + --hash=sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5 \ + --hash=sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab \ + --hash=sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572 \ + --hash=sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593 \ + --hash=sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29 \ + --hash=sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1 \ + --hash=sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f \ + --hash=sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8 \ + --hash=sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf \ + --hash=sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246 \ + --hash=sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9 \ + --hash=sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011 \ + --hash=sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a \ + --hash=sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6 \ + --hash=sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8 \ + --hash=sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a \ + --hash=sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2 \ + --hash=sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c \ + --hash=sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6 \ + --hash=sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d + # via pydantic +sniffio==1.3.1 \ + --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ + --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc + # via anyio +typing-extensions==4.14.1 \ + --hash=sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36 \ + --hash=sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76 + # via + # anyio + # exceptiongroup + # pydantic + # pydantic-core + # typing-inspection +typing-inspection==0.4.1 \ + --hash=sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51 \ + --hash=sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28 + # via pydantic diff --git a/src/whitelabel/AGENTS.md b/src/templafy/AGENTS.md similarity index 95% rename from src/whitelabel/AGENTS.md rename to src/templafy/AGENTS.md index d0c6a69..5a7603b 100644 --- a/src/whitelabel/AGENTS.md +++ b/src/templafy/AGENTS.md @@ -20,7 +20,7 @@ ## PeerTube API -- We are wrapping the PeerTube API that adheres to the OpenAPI 3 standard. +- We are wrapping the Templafy API that adheres to the OpenAPI 3 standard. - The JSON specification file can be found in `assets\openapi.json`. - All API endpoints information is contained there. - Note that the file might contain typos; do not correct them in the original JSON file, but correct them when using the text in the wrappers. diff --git a/src/templafy/__init__.py b/src/templafy/__init__.py new file mode 100644 index 0000000..b9f6612 --- /dev/null +++ b/src/templafy/__init__.py @@ -0,0 +1,114 @@ +"""A Python client for the Templafy API.""" + +# Trigger CI/CD to verify fixes + +from typing import Any + +__version__ = "0.1.0" + +# Type stubs for lazy-loaded items to satisfy pyright +# These will be overridden by __getattr__ at runtime +if False: # pragma: no cover + from . import api + from .client import AuthenticatedClient, Client + from .errors import ( + AuthenticationError, + AuthorizationError, + NotFoundError, + RateLimitError, + ServerError, + TemplafyError, + UnexpectedStatusError, + ValidationError, + ) + +# Additional type declarations to satisfy pyright's __all__ checking +# These are only for type checking and will be replaced by __getattr__ at runtime +else: + # Client classes + AuthenticatedClient: type + Client: type + + # Error classes + AuthenticationError: type + AuthorizationError: type + NotFoundError: type + RateLimitError: type + ServerError: type + TemplafyError: type + UnexpectedStatusError: type + ValidationError: type + + +# For now, defer imports that have external dependencies to avoid issues +# during development where dependencies may not be installed +def __getattr__(name: str) -> Any: + """Lazy imports for components with external dependencies.""" + if name == "Client": + from .client import Client # noqa: PLC0415 + + return Client + elif name == "AuthenticatedClient": + from .client import AuthenticatedClient # noqa: PLC0415 + + return AuthenticatedClient + elif name in [ + "TemplafyError", + "AuthenticationError", + "AuthorizationError", + "NotFoundError", + "ValidationError", + "RateLimitError", + "ServerError", + "UnexpectedStatusError", + ]: + from .errors import ( # noqa: PLC0415 + AuthenticationError, # noqa: F401 + AuthorizationError, # noqa: F401 + NotFoundError, # noqa: F401 + RateLimitError, # noqa: F401 + ServerError, # noqa: F401 + TemplafyError, # noqa: F401 + UnexpectedStatusError, # noqa: F401 + ValidationError, # noqa: F401 + ) + + return locals()[name] + else: + error_message = f"module '{__name__}' has no attribute '{name}'" + raise AttributeError(error_message) + + +# Export models (these have no external dependencies) +from .models import ( # noqa: E402 + Document, + Folder, + Image, + Library, + Link, + Slide, + Space, + Spreadsheet, +) + +__all__ = [ + "AuthenticatedClient", + "AuthenticationError", + "AuthorizationError", + "Client", + "Document", + "Folder", + "Image", + "Library", + "Link", + "NotFoundError", + "RateLimitError", + "ServerError", + "Slide", + "Space", + "Spreadsheet", + "TemplafyError", + "UnexpectedStatusError", + "ValidationError", + "api", +] diff --git a/src/templafy/api/__init__.py b/src/templafy/api/__init__.py new file mode 100644 index 0000000..af87038 --- /dev/null +++ b/src/templafy/api/__init__.py @@ -0,0 +1,15 @@ +"""API endpoint modules for the Templafy API.""" + +# Re-export all API modules for easy importing +from . import documents, folders, images, libraries, links, slides, spaces, spreadsheets + +__all__ = [ + "documents", + "folders", + "images", + "libraries", + "links", + "slides", + "spaces", + "spreadsheets", +] diff --git a/src/templafy/api/documents.py b/src/templafy/api/documents.py new file mode 100644 index 0000000..383344a --- /dev/null +++ b/src/templafy/api/documents.py @@ -0,0 +1,79 @@ +"""Documents API endpoints for the Templafy API.""" + +from templafy.client import AuthenticatedClient, Client +from templafy.errors import get_error_from_response +from templafy.models.document import Document + + +def get_documents( + *, + client: Client | AuthenticatedClient, + library_id: str | None = None, + folder_id: str | None = None, +) -> list[Document]: + """List documents. + + Args: + client: The API client to use + library_id: Optional library ID to filter by + folder_id: Optional folder ID to filter by + + Returns: + List of documents + + Raises: + TemplafyError: If the API request fails + """ + url = f"{client.base_url}/documents" + + params = {} + if library_id: + params["libraryId"] = library_id + if folder_id: + params["folderId"] = folder_id + + headers = {} + if isinstance(client, AuthenticatedClient): + headers = client.get_headers() + + response = client._client.get(url, headers=headers, params=params) # noqa: SLF001 + + if response.status_code == 200: + data = response.json() + return [Document(**item) for item in data] + else: + error = get_error_from_response(response) + raise error + + +def get_document( + *, + client: Client | AuthenticatedClient, + document_id: str, +) -> Document: + """Get a specific document. + + Args: + client: The API client to use + document_id: The ID of the document + + Returns: + The document + + Raises: + TemplafyError: If the API request fails + """ + url = f"{client.base_url}/documents/{document_id}" + + headers = {} + if isinstance(client, AuthenticatedClient): + headers = client.get_headers() + + response = client._client.get(url, headers=headers) # noqa: SLF001 + + if response.status_code == 200: + data = response.json() + return Document(**data) + else: + error = get_error_from_response(response) + raise error diff --git a/src/templafy/api/folders.py b/src/templafy/api/folders.py new file mode 100644 index 0000000..52a2cfb --- /dev/null +++ b/src/templafy/api/folders.py @@ -0,0 +1,36 @@ +"""Folders API endpoints for the Templafy API.""" + +from templafy.client import AuthenticatedClient, Client +from templafy.errors import get_error_from_response +from templafy.models.folder import Folder + + +def get_folders( + *, + client: Client | AuthenticatedClient, +) -> list[Folder]: + """List folders. + + Args: + client: The API client to use + + Returns: + List of folders + + Raises: + TemplafyError: If the API request fails + """ + url = f"{client.base_url}/folders" + + headers = {} + if isinstance(client, AuthenticatedClient): + headers = client.get_headers() + + response = client._client.get(url, headers=headers) # noqa: SLF001 + + if response.status_code == 200: + data = response.json() + return [Folder(**item) for item in data] + else: + error = get_error_from_response(response) + raise error diff --git a/src/templafy/api/images.py b/src/templafy/api/images.py new file mode 100644 index 0000000..1a61bca --- /dev/null +++ b/src/templafy/api/images.py @@ -0,0 +1,36 @@ +"""Images API endpoints for the Templafy API.""" + +from templafy.client import AuthenticatedClient, Client +from templafy.errors import get_error_from_response +from templafy.models.image import Image + + +def get_images( + *, + client: Client | AuthenticatedClient, +) -> list[Image]: + """List images. + + Args: + client: The API client to use + + Returns: + List of images + + Raises: + TemplafyError: If the API request fails + """ + url = f"{client.base_url}/images" + + headers = {} + if isinstance(client, AuthenticatedClient): + headers = client.get_headers() + + response = client._client.get(url, headers=headers) # noqa: SLF001 + + if response.status_code == 200: + data = response.json() + return [Image(**item) for item in data] + else: + error = get_error_from_response(response) + raise error diff --git a/src/templafy/api/libraries.py b/src/templafy/api/libraries.py new file mode 100644 index 0000000..c6cbaa1 --- /dev/null +++ b/src/templafy/api/libraries.py @@ -0,0 +1,36 @@ +"""Libraries API endpoints for the Templafy API.""" + +from templafy.client import AuthenticatedClient, Client +from templafy.errors import get_error_from_response +from templafy.models.library import Library + + +def get_libraries( + *, + client: Client | AuthenticatedClient, +) -> list[Library]: + """List libraries. + + Args: + client: The API client to use + + Returns: + List of libraries + + Raises: + TemplafyError: If the API request fails + """ + url = f"{client.base_url}/libraries" + + headers = {} + if isinstance(client, AuthenticatedClient): + headers = client.get_headers() + + response = client._client.get(url, headers=headers) # noqa: SLF001 + + if response.status_code == 200: + data = response.json() + return [Library(**item) for item in data] + else: + error = get_error_from_response(response) + raise error diff --git a/src/templafy/api/links.py b/src/templafy/api/links.py new file mode 100644 index 0000000..a3a8613 --- /dev/null +++ b/src/templafy/api/links.py @@ -0,0 +1,36 @@ +"""Links API endpoints for the Templafy API.""" + +from templafy.client import AuthenticatedClient, Client +from templafy.errors import get_error_from_response +from templafy.models.link import Link + + +def get_links( + *, + client: Client | AuthenticatedClient, +) -> list[Link]: + """List links. + + Args: + client: The API client to use + + Returns: + List of links + + Raises: + TemplafyError: If the API request fails + """ + url = f"{client.base_url}/links" + + headers = {} + if isinstance(client, AuthenticatedClient): + headers = client.get_headers() + + response = client._client.get(url, headers=headers) # noqa: SLF001 + + if response.status_code == 200: + data = response.json() + return [Link(**item) for item in data] + else: + error = get_error_from_response(response) + raise error diff --git a/src/templafy/api/slides.py b/src/templafy/api/slides.py new file mode 100644 index 0000000..646d717 --- /dev/null +++ b/src/templafy/api/slides.py @@ -0,0 +1,36 @@ +"""Slides API endpoints for the Templafy API.""" + +from templafy.client import AuthenticatedClient, Client +from templafy.errors import get_error_from_response +from templafy.models.slide import Slide + + +def get_slides( + *, + client: Client | AuthenticatedClient, +) -> list[Slide]: + """List slides. + + Args: + client: The API client to use + + Returns: + List of slides + + Raises: + TemplafyError: If the API request fails + """ + url = f"{client.base_url}/slides" + + headers = {} + if isinstance(client, AuthenticatedClient): + headers = client.get_headers() + + response = client._client.get(url, headers=headers) # noqa: SLF001 + + if response.status_code == 200: + data = response.json() + return [Slide(**item) for item in data] + else: + error = get_error_from_response(response) + raise error diff --git a/src/templafy/api/spaces.py b/src/templafy/api/spaces.py new file mode 100644 index 0000000..0df1873 --- /dev/null +++ b/src/templafy/api/spaces.py @@ -0,0 +1,70 @@ +"""Spaces API endpoints for the Templafy API.""" + +import httpx + +from templafy.client import AuthenticatedClient, Client +from templafy.errors import get_error_from_response +from templafy.models.space import Space + + +def get_spaces( + *, + client: Client | AuthenticatedClient, +) -> list[Space]: + """List all existing active spaces. + + Args: + client: The API client to use + + Returns: + List of spaces + + Raises: + TemplafyError: If the API request fails + """ + url = f"{client.base_url}/spaces" + + headers = {} + if isinstance(client, AuthenticatedClient): + headers = client.get_headers() + + response = client._client.get(url, headers=headers) # noqa: SLF001 + + if response.status_code == 200: + data = response.json() + return [Space(**item) for item in data] + else: + error = get_error_from_response(response) + raise error + + +async def get_spaces_async( + *, + client: Client | AuthenticatedClient, +) -> list[Space]: + """List all existing active spaces (async version). + + Args: + client: The API client to use + + Returns: + List of spaces + + Raises: + TemplafyError: If the API request fails + """ + url = f"{client.base_url}/spaces" + + headers = {} + if isinstance(client, AuthenticatedClient): + headers = client.get_headers() + + async with httpx.AsyncClient() as async_client: + response = await async_client.get(url, headers=headers) + + if response.status_code == 200: + data = response.json() + return [Space(**item) for item in data] + else: + error = get_error_from_response(response) + raise error diff --git a/src/templafy/api/spreadsheets.py b/src/templafy/api/spreadsheets.py new file mode 100644 index 0000000..22784ef --- /dev/null +++ b/src/templafy/api/spreadsheets.py @@ -0,0 +1,36 @@ +"""Spreadsheets API endpoints for the Templafy API.""" + +from templafy.client import AuthenticatedClient, Client +from templafy.errors import get_error_from_response +from templafy.models.spreadsheet import Spreadsheet + + +def get_spreadsheets( + *, + client: Client | AuthenticatedClient, +) -> list[Spreadsheet]: + """List spreadsheets. + + Args: + client: The API client to use + + Returns: + List of spreadsheets + + Raises: + TemplafyError: If the API request fails + """ + url = f"{client.base_url}/spreadsheets" + + headers = {} + if isinstance(client, AuthenticatedClient): + headers = client.get_headers() + + response = client._client.get(url, headers=headers) # noqa: SLF001 + + if response.status_code == 200: + data = response.json() + return [Spreadsheet(**item) for item in data] + else: + error = get_error_from_response(response) + raise error diff --git a/src/templafy/client.py b/src/templafy/client.py new file mode 100644 index 0000000..25e6f3e --- /dev/null +++ b/src/templafy/client.py @@ -0,0 +1,86 @@ +"""Base client classes for the Templafy API.""" + +import httpx + + +class Client: + """A client for the Templafy API.""" + + def __init__( + self, + base_url: str, + *, + httpx_client: httpx.Client | None = None, + timeout: float = 10.0, + verify_ssl: bool = True, + ) -> None: + """Initialize the client. + + Args: + base_url: The base URL for the API + httpx_client: An optional httpx client to use + timeout: Request timeout in seconds + verify_ssl: Whether to verify SSL certificates + """ + self.base_url = base_url.rstrip("/") + self.timeout = timeout + self.verify_ssl = verify_ssl + self._client = httpx_client or httpx.Client( + timeout=timeout, + verify=verify_ssl, + ) + + def __enter__(self) -> "Client": + """Enter context manager.""" + return self + + def __exit__(self, *args: object) -> None: + """Exit context manager.""" + self._client.close() + + def close(self) -> None: + """Close the client.""" + self._client.close() + + +class AuthenticatedClient(Client): + """An authenticated client for the Templafy API.""" + + def __init__( + self, + base_url: str, + token: str, + *, + httpx_client: httpx.Client | None = None, + timeout: float = 10.0, + verify_ssl: bool = True, + ) -> None: + """Initialize the authenticated client. + + Args: + base_url: The base URL for the API + token: The API token for authentication + httpx_client: An optional httpx client to use + timeout: Request timeout in seconds + verify_ssl: Whether to verify SSL certificates + """ + super().__init__( + base_url=base_url, + httpx_client=httpx_client, + timeout=timeout, + verify_ssl=verify_ssl, + ) + self.token = token + if httpx_client is None: + self._client = httpx.Client( + timeout=timeout, + verify=verify_ssl, + headers={"Authorization": f"Bearer {token}"}, + ) + + def get_headers(self) -> dict[str, str]: + """Get the headers for authenticated requests.""" + return { + "Authorization": f"Bearer {self.token}", + "Content-Type": "application/json", + } diff --git a/src/templafy/errors.py b/src/templafy/errors.py new file mode 100644 index 0000000..0d3b5d5 --- /dev/null +++ b/src/templafy/errors.py @@ -0,0 +1,92 @@ +"""Error classes and exceptions for the Templafy API client.""" + +import httpx + + +class TemplafyError(Exception): + """Base exception for Templafy API errors.""" + + def __init__(self, message: str, response: httpx.Response | None = None) -> None: + """Initialize the error. + + Args: + message: Error message + response: HTTP response that caused the error + """ + super().__init__(message) + self.message = message + self.response = response + + +class AuthenticationError(TemplafyError): + """Authentication failed.""" + + +class AuthorizationError(TemplafyError): + """Authorization failed - insufficient permissions.""" + + +class NotFoundError(TemplafyError): + """Resource not found.""" + + +class ValidationError(TemplafyError): + """Request validation failed.""" + + +class RateLimitError(TemplafyError): + """Rate limit exceeded.""" + + +class ServerError(TemplafyError): + """Server error (5xx status codes).""" + + +class UnexpectedStatusError(TemplafyError): + """Unexpected HTTP status code.""" + + def __init__( + self, + status_code: int, + content: bytes, + response: httpx.Response | None = None, + ) -> None: + """Initialize unexpected status error. + + Args: + status_code: HTTP status code + content: Response content + response: HTTP response + """ + message = f"Unexpected status code: {status_code}" + super().__init__(message, response) + self.status_code = status_code + self.content = content + + +def get_error_from_response(response: httpx.Response) -> TemplafyError: + """Get appropriate error from HTTP response. + + Args: + response: HTTP response + + Returns: + Appropriate error instance + """ + status_code = response.status_code + content = response.content + + error_map = { + 401: lambda: AuthenticationError("Authentication failed", response), + 403: lambda: AuthorizationError("Authorization failed", response), + 404: lambda: NotFoundError("Resource not found", response), + 422: lambda: ValidationError("Request validation failed", response), + 429: lambda: RateLimitError("Rate limit exceeded", response), + } + + if status_code in error_map: + return error_map[status_code]() + elif status_code >= 500: + return ServerError(f"Server error: {status_code}", response) + else: + return UnexpectedStatusError(status_code, content, response) diff --git a/src/templafy/models/__init__.py b/src/templafy/models/__init__.py new file mode 100644 index 0000000..1cb41d7 --- /dev/null +++ b/src/templafy/models/__init__.py @@ -0,0 +1,97 @@ +"""Models for the Templafy API.""" + +from typing import Any + +# For now use simple models without pydantic to avoid dependency issues +# during development +from .space_simple import Space + + +# Create simple placeholder classes for other models +class Document: + """A Templafy document template.""" + + def __init__(self, document_id: str, name: str, **kwargs: Any) -> None: + """Initialize a Document.""" + self.id = document_id + self.name = name + for key, value in kwargs.items(): + setattr(self, key, value) + + +class Library: + """A Templafy library.""" + + def __init__(self, library_id: str, name: str, **kwargs: Any) -> None: + """Initialize a Library.""" + self.id = library_id + self.name = name + for key, value in kwargs.items(): + setattr(self, key, value) + + +class Folder: + """A Templafy folder.""" + + def __init__(self, folder_id: str, name: str, **kwargs: Any) -> None: + """Initialize a Folder.""" + self.id = folder_id + self.name = name + for key, value in kwargs.items(): + setattr(self, key, value) + + +class Image: + """A Templafy image asset.""" + + def __init__(self, image_id: str, name: str, **kwargs: Any) -> None: + """Initialize an Image.""" + self.id = image_id + self.name = name + for key, value in kwargs.items(): + setattr(self, key, value) + + +class Slide: + """A Templafy slide template.""" + + def __init__(self, slide_id: str, name: str, **kwargs: Any) -> None: + """Initialize a Slide.""" + self.id = slide_id + self.name = name + for key, value in kwargs.items(): + setattr(self, key, value) + + +class Spreadsheet: + """A Templafy spreadsheet template.""" + + def __init__(self, spreadsheet_id: str, name: str, **kwargs: Any) -> None: + """Initialize a Spreadsheet.""" + self.id = spreadsheet_id + self.name = name + for key, value in kwargs.items(): + setattr(self, key, value) + + +class Link: + """A Templafy link asset.""" + + def __init__(self, link_id: str, name: str, **kwargs: Any) -> None: + """Initialize a Link.""" + self.id = link_id + self.name = name + for key, value in kwargs.items(): + setattr(self, key, value) + + +__all__ = [ + "Document", + "Folder", + "Image", + "Library", + "Link", + "Slide", + "Space", + "Spreadsheet", +] diff --git a/src/templafy/models/document.py b/src/templafy/models/document.py new file mode 100644 index 0000000..3cb5ef5 --- /dev/null +++ b/src/templafy/models/document.py @@ -0,0 +1,21 @@ +"""Document model for the Templafy API.""" + +from pydantic import BaseModel, ConfigDict + + +class Document(BaseModel): + """A Templafy document template.""" + + id: str + name: str + description: str | None = None + template_type: str | None = None + library_id: str | None = None + folder_id: str | None = None + tags: list[str] | None = None + created_at: str | None = None + updated_at: str | None = None + size: int | None = None + download_url: str | None = None + + model_config = ConfigDict(extra="allow") diff --git a/src/templafy/models/folder.py b/src/templafy/models/folder.py new file mode 100644 index 0000000..26ea0c8 --- /dev/null +++ b/src/templafy/models/folder.py @@ -0,0 +1,16 @@ +"""Folder model for the Templafy API.""" + +from pydantic import BaseModel, ConfigDict + + +class Folder(BaseModel): + """A Templafy folder.""" + + id: str + name: str + parent_id: str | None = None + library_id: str | None = None + created_at: str | None = None + updated_at: str | None = None + + model_config = ConfigDict(extra="allow") diff --git a/src/templafy/models/image.py b/src/templafy/models/image.py new file mode 100644 index 0000000..789e0e3 --- /dev/null +++ b/src/templafy/models/image.py @@ -0,0 +1,24 @@ +"""Image model for the Templafy API.""" + +from pydantic import BaseModel, ConfigDict + + +class Image(BaseModel): + """A Templafy image asset.""" + + id: str + name: str + description: str | None = None + library_id: str | None = None + folder_id: str | None = None + tags: list[str] | None = None + file_size: int | None = None + width: int | None = None + height: int | None = None + format: str | None = None + download_url: str | None = None + thumbnail_url: str | None = None + created_at: str | None = None + updated_at: str | None = None + + model_config = ConfigDict(extra="allow") diff --git a/src/templafy/models/library.py b/src/templafy/models/library.py new file mode 100644 index 0000000..5d7685a --- /dev/null +++ b/src/templafy/models/library.py @@ -0,0 +1,17 @@ +"""Library model for the Templafy API.""" + +from pydantic import BaseModel, ConfigDict + + +class Library(BaseModel): + """A Templafy library.""" + + id: str + name: str + description: str | None = None + space_id: str | None = None + is_active: bool = True + created_at: str | None = None + updated_at: str | None = None + + model_config = ConfigDict(extra="allow") diff --git a/src/templafy/models/link.py b/src/templafy/models/link.py new file mode 100644 index 0000000..e05dcba --- /dev/null +++ b/src/templafy/models/link.py @@ -0,0 +1,19 @@ +"""Link model for the Templafy API.""" + +from pydantic import BaseModel, ConfigDict + + +class Link(BaseModel): + """A Templafy link asset.""" + + id: str + name: str + description: str | None = None + url: str + library_id: str | None = None + folder_id: str | None = None + tags: list[str] | None = None + created_at: str | None = None + updated_at: str | None = None + + model_config = ConfigDict(extra="allow") diff --git a/src/templafy/models/slide.py b/src/templafy/models/slide.py new file mode 100644 index 0000000..002e6b9 --- /dev/null +++ b/src/templafy/models/slide.py @@ -0,0 +1,21 @@ +"""Slide model for the Templafy API.""" + +from pydantic import BaseModel, ConfigDict + + +class Slide(BaseModel): + """A Templafy slide template.""" + + id: str + name: str + description: str | None = None + library_id: str | None = None + folder_id: str | None = None + tags: list[str] | None = None + slide_number: int | None = None + layout_name: str | None = None + thumbnail_url: str | None = None + created_at: str | None = None + updated_at: str | None = None + + model_config = ConfigDict(extra="allow") diff --git a/src/templafy/models/space.py b/src/templafy/models/space.py new file mode 100644 index 0000000..c2b1b2d --- /dev/null +++ b/src/templafy/models/space.py @@ -0,0 +1,16 @@ +"""Space model for the Templafy API.""" + +from pydantic import BaseModel, ConfigDict + + +class Space(BaseModel): + """A Templafy space (workspace/tenant).""" + + id: str + name: str + description: str | None = None + is_active: bool = True + created_at: str | None = None + updated_at: str | None = None + + model_config = ConfigDict(extra="allow") diff --git a/src/templafy/models/space_simple.py b/src/templafy/models/space_simple.py new file mode 100644 index 0000000..e5120bf --- /dev/null +++ b/src/templafy/models/space_simple.py @@ -0,0 +1,44 @@ +"""Simple space model for the Templafy API (no external dependencies).""" + +from typing import Any + + +class Space: + """A Templafy space (workspace/tenant).""" + + def __init__( # noqa: PLR0913 + self, + space_id: str, + name: str, + description: str | None = None, + *, + is_active: bool = True, + created_at: str | None = None, + updated_at: str | None = None, + **kwargs: Any, + ) -> None: + """Initialize a Space. + + Args: + space_id: Space ID + name: Space name + description: Space description + is_active: Whether the space is active + created_at: Creation timestamp + updated_at: Update timestamp + **kwargs: Additional fields + """ + self.id = space_id + self.name = name + self.description = description + self.is_active = is_active + self.created_at = created_at + self.updated_at = updated_at + + # Store additional fields + for key, value in kwargs.items(): + setattr(self, key, value) + + def __repr__(self) -> str: + """Return string representation.""" + return f"Space(id='{self.id}', name='{self.name}')" diff --git a/src/templafy/models/spreadsheet.py b/src/templafy/models/spreadsheet.py new file mode 100644 index 0000000..af2388e --- /dev/null +++ b/src/templafy/models/spreadsheet.py @@ -0,0 +1,20 @@ +"""Spreadsheet model for the Templafy API.""" + +from pydantic import BaseModel, ConfigDict + + +class Spreadsheet(BaseModel): + """A Templafy spreadsheet template.""" + + id: str + name: str + description: str | None = None + library_id: str | None = None + folder_id: str | None = None + tags: list[str] | None = None + file_size: int | None = None + download_url: str | None = None + created_at: str | None = None + updated_at: str | None = None + + model_config = ConfigDict(extra="allow") diff --git a/src/templafy/types.py b/src/templafy/types.py new file mode 100644 index 0000000..6bc4deb --- /dev/null +++ b/src/templafy/types.py @@ -0,0 +1,23 @@ +"""Common type definitions for the Templafy API client.""" + +from typing import Any, Literal, TypeAlias + +# Response types +Response: TypeAlias = dict[str, Any] +ResponseList: TypeAlias = list[dict[str, Any]] + +# HTTP methods +HTTPMethod: TypeAlias = Literal["GET", "POST", "PUT", "DELETE", "PATCH"] + +# File types for upload/download +FileType: TypeAlias = bytes | str +FileDict: TypeAlias = dict[str, Any] + +# Common query parameters +QueryParams: TypeAlias = dict[str, str | int | bool | list[str]] | None + +# Headers +Headers: TypeAlias = dict[str, str] + +# JSON data +JSONType: TypeAlias = dict[str, Any] | list[Any] | str | int | float | bool | None diff --git a/src/whitelabel/__init__.py b/src/whitelabel/__init__.py deleted file mode 100644 index 4c1858c..0000000 --- a/src/whitelabel/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -"""A minimal Python package template.""" - -__version__ = "0.1.0" - -from .functions.hello_world import hello_world - -__all__ = ["hello_world"] diff --git a/src/whitelabel/functions/__init__.py b/src/whitelabel/functions/__init__.py deleted file mode 100644 index dd1cc87..0000000 --- a/src/whitelabel/functions/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Functions module for the whitelabel package.""" - -from .hello_world import hello_world - -__all__ = ["hello_world"] diff --git a/src/whitelabel/functions/hello_world.py b/src/whitelabel/functions/hello_world.py deleted file mode 100644 index 93a6eae..0000000 --- a/src/whitelabel/functions/hello_world.py +++ /dev/null @@ -1,20 +0,0 @@ -"""A simple hello world function for the template.""" - - -def hello_world(name: str = "World") -> str: - """Return a greeting message. - - Args: - name: The name to greet. Defaults to "World". - - Returns: - A greeting string. - - Examples: - >>> hello_world() - 'Hello, World!' - - >>> hello_world("Python") - 'Hello, Python!' - """ - return f"Hello, {name}!" diff --git a/tests/conftest.py b/tests/conftest.py index 3a91541..a27c6fb 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,6 +2,8 @@ import pytest +from templafy import AuthenticatedClient, Client + collect_ignore_glob = ["assets/**"] pytest_plugins = [] @@ -10,3 +12,81 @@ def assets_dir() -> Path: """Return a path to the test assets directory.""" return Path(__file__).parent / "assets" + + +@pytest.fixture +def base_url(): + """Base URL for testing.""" + return "https://test.api.templafy.com/v3" + + +@pytest.fixture +def api_token(): + """Mock API token for testing.""" + return "test_token_12345" + + +@pytest.fixture +def mock_client(base_url): + """Mock unauthenticated client for testing.""" + return Client(base_url=base_url) + + +@pytest.fixture +def mock_authenticated_client(base_url, api_token): + """Mock authenticated client for testing.""" + return AuthenticatedClient(base_url=base_url, token=api_token) + + +@pytest.fixture +def mock_space_data(): + """Mock space data for testing.""" + return [ + { + "id": "space1", + "name": "Test Space 1", + "description": "A test space", + "is_active": True, + }, + { + "id": "space2", + "name": "Test Space 2", + "description": "Another test space", + "is_active": True, + }, + ] + + +@pytest.fixture +def mock_document_data(): + """Mock document data for testing.""" + return [ + { + "id": "doc1", + "name": "Test Document 1", + "description": "A test document", + "template_type": "word", + "library_id": "lib1", + }, + { + "id": "doc2", + "name": "Test Document 2", + "description": "Another test document", + "template_type": "powerpoint", + "library_id": "lib1", + }, + ] + + +@pytest.fixture +def mock_library_data(): + """Mock library data for testing.""" + return [ + { + "id": "lib1", + "name": "Test Library 1", + "description": "A test library", + "space_id": "space1", + "is_active": True, + }, + ] diff --git a/tests/templafy/api/test_documents.py b/tests/templafy/api/test_documents.py new file mode 100644 index 0000000..6851625 --- /dev/null +++ b/tests/templafy/api/test_documents.py @@ -0,0 +1,32 @@ +"""Tests for the documents API.""" + +from unittest.mock import Mock, patch + +from templafy.api.documents import get_document, get_documents +from templafy.models.document import Document + + +def test_get_documents_success(mock_authenticated_client, mock_document_data): + """Test successful documents listing returns document list.""" + with patch.object(mock_authenticated_client._client, "get") as mock_get: # noqa: SLF001 + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = mock_document_data + mock_get.return_value = mock_response + + result = get_documents(client=mock_authenticated_client) + + assert len(result) == len(mock_document_data) + + +def test_get_document_success(mock_authenticated_client, mock_document_data): + """Test successful single document retrieval.""" + with patch.object(mock_authenticated_client._client, "get") as mock_get: # noqa: SLF001 + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = mock_document_data[0] + mock_get.return_value = mock_response + + result = get_document(client=mock_authenticated_client, document_id="doc1") + + assert isinstance(result, Document) diff --git a/tests/templafy/api/test_spaces.py b/tests/templafy/api/test_spaces.py new file mode 100644 index 0000000..884de72 --- /dev/null +++ b/tests/templafy/api/test_spaces.py @@ -0,0 +1,32 @@ +"""Tests for the spaces API.""" + +from unittest.mock import Mock, patch + +from templafy.api.spaces import get_spaces +from templafy.models.space import Space + + +def test_get_spaces_success(mock_authenticated_client, mock_space_data): + """Test successful spaces listing returns space list.""" + with patch.object(mock_authenticated_client._client, "get") as mock_get: # noqa: SLF001 + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = mock_space_data + mock_get.return_value = mock_response + + result = get_spaces(client=mock_authenticated_client) + + assert len(result) == len(mock_space_data) + + +def test_get_spaces_returns_space_objects(mock_authenticated_client, mock_space_data): + """Test that get_spaces returns Space objects.""" + with patch.object(mock_authenticated_client._client, "get") as mock_get: # noqa: SLF001 + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = mock_space_data + mock_get.return_value = mock_response + + result = get_spaces(client=mock_authenticated_client) + + assert all(isinstance(space, Space) for space in result) diff --git a/tests/templafy/models/test_space.py b/tests/templafy/models/test_space.py new file mode 100644 index 0000000..365380a --- /dev/null +++ b/tests/templafy/models/test_space.py @@ -0,0 +1,30 @@ +"""Tests for the Space model.""" + +from templafy.models.space import Space + + +def test_space_model_creation(): + """Test Space model can be created with required fields.""" + space_data = { + "id": "space1", + "name": "Test Space", + "description": "A test space", + "is_active": True, + } + + space = Space(**space_data) + + assert space.id == "space1" + assert space.name == "Test Space" + assert space.description == "A test space" + assert space.is_active is True + + +def test_space_model_with_minimal_data(): + """Test Space model with minimal required data.""" + space = Space(id="space2", name="Minimal Space") + + assert space.id == "space2" + assert space.name == "Minimal Space" + assert space.description is None + assert space.is_active is True diff --git a/tests/templafy/test_client.py b/tests/templafy/test_client.py new file mode 100644 index 0000000..226a236 --- /dev/null +++ b/tests/templafy/test_client.py @@ -0,0 +1,23 @@ +"""Tests for the Templafy API client.""" + +from templafy import AuthenticatedClient, Client + + +def test_client_initialization(base_url): + """Test client initialization with base URL.""" + client = Client(base_url=base_url) + assert client.base_url == base_url + + +def test_authenticated_client_initialization(base_url, api_token): + """Test authenticated client initialization.""" + client = AuthenticatedClient(base_url=base_url, token=api_token) + assert client.base_url == base_url + assert client.token == api_token + + +def test_authenticated_client_headers(mock_authenticated_client, api_token): + """Test authenticated client generates correct headers.""" + headers = mock_authenticated_client.get_headers() + assert headers["Authorization"] == f"Bearer {api_token}" + assert headers["Content-Type"] == "application/json" diff --git a/tests/whitelabel/test_hello_world.py b/tests/whitelabel/test_hello_world.py deleted file mode 100644 index 852f782..0000000 --- a/tests/whitelabel/test_hello_world.py +++ /dev/null @@ -1,9 +0,0 @@ -"""Tests for the hello_world function.""" - -from whitelabel.functions.hello_world import hello_world - - -def test_hello_world(): - """Test hello_world function returns expected greeting.""" - result = hello_world("World") - assert result == "Hello, World!" diff --git a/uv.lock b/uv.lock index a41f865..9be51d6 100644 --- a/uv.lock +++ b/uv.lock @@ -15,6 +15,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, ] +[[package]] +name = "anyio" +version = "4.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/b4/636b3b65173d3ce9a38ef5f0522789614e590dab6a8d505340a4efe4c567/anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6", size = 213252, upload-time = "2025-08-04T08:54:26.451Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1", size = 107213, upload-time = "2025-08-04T08:54:24.882Z" }, +] + [[package]] name = "appnope" version = "0.1.4" @@ -772,6 +787,43 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/86/52/b6bbef2d40d0ec7bed990996da67b68d507bc2ee2e2e34930c64b1ebd7d7/grimp-3.9-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:7719cb213aacad7d0e6d69a9be4f998133d9b9ad3fa873b07dfaa221131ac2dc", size = 2093646, upload-time = "2025-05-05T13:46:46.179Z" }, ] +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + [[package]] name = "identify" version = "2.6.12" @@ -1526,6 +1578,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/e3/6690b3f85a05506733c7e90b577e4762517404ea78bab2ca3a5cb1aeb78d/numpy-2.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6936aff90dda378c09bea075af0d9c675fe3a977a9d2402f95a87f440f59f619", size = 12977811, upload-time = "2025-07-24T21:29:18.234Z" }, ] +[[package]] +name = "openapi-python-client" +version = "0.26.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "httpx" }, + { name = "jinja2" }, + { name = "pydantic" }, + { name = "python-dateutil" }, + { name = "ruamel-yaml" }, + { name = "ruff" }, + { name = "shellingham" }, + { name = "typer" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/31/264cf223b301186cfd2f3a78f962ee641e3b7794ec7b483380c56d59e824/openapi_python_client-0.26.1.tar.gz", hash = "sha256:e3832f0ef074a0ab591d1eeb5d3dab2ca820cd0349f7e79d9663b7b21206be5d", size = 126194, upload-time = "2025-09-13T05:49:34.514Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/56/d9304d6b7f8a173c868e10a506b8ccab79305d9e412b06cf89c654155f35/openapi_python_client-0.26.1-py3-none-any.whl", hash = "sha256:415cb8095b1a3f15cec45670c5075c5097f65390a351d21512e8f6ea5c1be644", size = 183638, upload-time = "2025-09-13T05:49:32.55Z" }, +] + [[package]] name = "packaging" version = "25.0" @@ -2437,6 +2511,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303, upload-time = "2025-01-02T07:14:38.724Z" }, ] +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + [[package]] name = "stack-data" version = "0.6.3" @@ -2451,6 +2534,79 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, ] +[[package]] +name = "templafy" +source = { editable = "." } +dependencies = [ + { name = "httpx" }, + { name = "pydantic" }, +] + +[package.dev-dependencies] +dev = [ + { name = "build" }, + { name = "deptry" }, + { name = "ipykernel" }, + { name = "ipython", version = "8.37.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "ipython", version = "9.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "ipywidgets" }, + { name = "matplotlib" }, + { name = "nbstripout" }, + { name = "openapi-python-client" }, + { name = "perfplot" }, + { name = "pip" }, + { name = "pre-commit" }, + { name = "pre-commit-update" }, + { name = "pyright", extra = ["nodejs"] }, + { name = "ruff" }, + { name = "tqdm" }, + { name = "usethis" }, +] +doc = [ + { name = "mkdocs" }, +] +test = [ + { name = "coverage", extra = ["toml"] }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-emoji" }, + { name = "pytest-md" }, +] + +[package.metadata] +requires-dist = [ + { name = "httpx", specifier = ">=0.24.0" }, + { name = "pydantic", specifier = ">=2.0.0" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "build", specifier = ">=1.2.2.post1" }, + { name = "deptry", specifier = ">=0.23.0" }, + { name = "ipykernel", specifier = ">=6.30.0" }, + { name = "ipython", specifier = ">=8.37.0" }, + { name = "ipywidgets", specifier = ">=8.1.7" }, + { name = "matplotlib", specifier = ">=3.10.3" }, + { name = "nbstripout", specifier = ">=0.8.1" }, + { name = "openapi-python-client", specifier = ">=0.15.0" }, + { name = "perfplot", specifier = ">=0.10.2" }, + { name = "pip", specifier = ">=25.1.1" }, + { name = "pre-commit", specifier = ">=4.2.0" }, + { name = "pre-commit-update", specifier = ">=0.8.0" }, + { name = "pyright", extras = ["nodejs"], specifier = ">=1.1.403" }, + { name = "ruff", specifier = ">=0.12.5" }, + { name = "tqdm", specifier = ">=4.67.1" }, + { name = "usethis", specifier = ">=0.15.2" }, +] +doc = [{ name = "mkdocs", specifier = ">=1.6.1" }] +test = [ + { name = "coverage", extras = ["toml"], specifier = ">=7.10.1" }, + { name = "pytest", specifier = ">=8.4.1" }, + { name = "pytest-cov", specifier = ">=6.2.1" }, + { name = "pytest-emoji", specifier = ">=0.2.0" }, + { name = "pytest-md", specifier = ">=0.2.0" }, +] + [[package]] name = "tomli" version = "2.2.1" @@ -2662,69 +2818,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166, upload-time = "2024-01-06T02:10:55.763Z" }, ] -[[package]] -name = "whitelabel" -source = { editable = "." } - -[package.dev-dependencies] -dev = [ - { name = "build" }, - { name = "deptry" }, - { name = "ipykernel" }, - { name = "ipython", version = "8.37.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "ipython", version = "9.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "ipywidgets" }, - { name = "matplotlib" }, - { name = "nbstripout" }, - { name = "perfplot" }, - { name = "pip" }, - { name = "pre-commit" }, - { name = "pre-commit-update" }, - { name = "pyright", extra = ["nodejs"] }, - { name = "ruff" }, - { name = "tqdm" }, - { name = "usethis" }, -] -doc = [ - { name = "mkdocs" }, -] -test = [ - { name = "coverage", extra = ["toml"] }, - { name = "pytest" }, - { name = "pytest-cov" }, - { name = "pytest-emoji" }, - { name = "pytest-md" }, -] - -[package.metadata] - -[package.metadata.requires-dev] -dev = [ - { name = "build", specifier = ">=1.2.2.post1" }, - { name = "deptry", specifier = ">=0.23.0" }, - { name = "ipykernel", specifier = ">=6.30.0" }, - { name = "ipython", specifier = ">=8.37.0" }, - { name = "ipywidgets", specifier = ">=8.1.7" }, - { name = "matplotlib", specifier = ">=3.10.3" }, - { name = "nbstripout", specifier = ">=0.8.1" }, - { name = "perfplot", specifier = ">=0.10.2" }, - { name = "pip", specifier = ">=25.1.1" }, - { name = "pre-commit", specifier = ">=4.2.0" }, - { name = "pre-commit-update", specifier = ">=0.8.0" }, - { name = "pyright", extras = ["nodejs"], specifier = ">=1.1.403" }, - { name = "ruff", specifier = ">=0.12.5" }, - { name = "tqdm", specifier = ">=4.67.1" }, - { name = "usethis", specifier = ">=0.15.2" }, -] -doc = [{ name = "mkdocs", specifier = ">=1.6.1" }] -test = [ - { name = "coverage", extras = ["toml"], specifier = ">=7.10.1" }, - { name = "pytest", specifier = ">=8.4.1" }, - { name = "pytest-cov", specifier = ">=6.2.1" }, - { name = "pytest-emoji", specifier = ">=0.2.0" }, - { name = "pytest-md", specifier = ">=0.2.0" }, -] - [[package]] name = "widgetsnbextension" version = "4.0.14"