diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index bd4ba6f..f3b2a7a 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -6,7 +6,7 @@ }, "metadata": { "description": "Claude scientific skills from K-Dense Inc", - "version": "2.23.0" + "version": "2.24.0" }, "plugins": [ { @@ -71,6 +71,7 @@ "./scientific-skills/pysam", "./scientific-skills/pytdc", "./scientific-skills/pytorch-lightning", + "./scientific-skills/pyzotero", "./scientific-skills/qiskit", "./scientific-skills/qutip", "./scientific-skills/rdkit", diff --git a/README.md b/README.md index 00a5e83..df97a48 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ # Claude Scientific Skills [](LICENSE.md) -[](#whats-included) +[](#whats-included) [](https://agentskills.io/) [](#getting-started) -A comprehensive collection of **147+ ready-to-use scientific and research skills** (now including financial/SEC research, U.S. Treasury fiscal data, OFR Hedge Fund Monitor, and Alpha Vantage market data) for any AI agent that supports the open [Agent Skills](https://agentskills.io/) standard, created by [K-Dense](https://k-dense.ai). Works with **Cursor, Claude Code, Codex, and more**. Transform your AI agent into a research assistant capable of executing complex multi-step scientific workflows across biology, chemistry, medicine, and beyond. +A comprehensive collection of **148+ ready-to-use scientific and research skills** (now including financial/SEC research, U.S. Treasury fiscal data, OFR Hedge Fund Monitor, and Alpha Vantage market data) for any AI agent that supports the open [Agent Skills](https://agentskills.io/) standard, created by [K-Dense](https://k-dense.ai). Works with **Cursor, Claude Code, Codex, and more**. Transform your AI agent into a research assistant capable of executing complex multi-step scientific workflows across biology, chemistry, medicine, and beyond. **Looking for the full AI co-scientist experience?** Try [K-Dense Web](https://k-dense.ai) for 200+ skills, cloud compute, and publication-ready outputs. @@ -68,10 +68,10 @@ These skills enable your AI agent to seamlessly work with specialized scientific ## 📦 What's Included -This repository provides **147 scientific and research skills** organized into the following categories: +This repository provides **148 scientific and research skills** organized into the following categories: - **30+ Scientific & Financial Databases** - Direct API access to OpenAlex, PubMed, bioRxiv, ChEMBL, UniProt, COSMIC, ClinicalTrials.gov, SEC EDGAR, U.S. Treasury Fiscal Data, Alpha Vantage, and more -- **55+ Python Packages** - RDKit, Scanpy, PyTorch Lightning, scikit-learn, BioPython, BioServices, PennyLane, Qiskit, and others +- **55+ Python Packages** - RDKit, Scanpy, PyTorch Lightning, scikit-learn, BioPython, pyzotero, BioServices, PennyLane, Qiskit, and others - **15+ Scientific Integrations** - Benchling, DNAnexus, LatchBio, OMERO, Protocols.io, and more - **30+ Analysis & Communication Tools** - Literature review, scientific writing, peer review, document processing, posters, slides, schematics, and more - **10+ Research & Clinical Tools** - Hypothesis generation, grant writing, clinical decision support, treatment plans, regulatory compliance @@ -113,7 +113,7 @@ Each skill includes: - **Multi-Step Workflows** - Execute complex pipelines with a single prompt ### 🎯 **Comprehensive Coverage** -- **140 Skills** - Extensive coverage across all major scientific domains +- **148 Skills** - Extensive coverage across all major scientific domains - **28+ Databases** - Direct access to OpenAlex, PubMed, bioRxiv, ChEMBL, UniProt, COSMIC, and more - **55+ Python Packages** - RDKit, Scanpy, PyTorch Lightning, scikit-learn, BioServices, PennyLane, Qiskit, and others diff --git a/docs/scientific-skills.md b/docs/scientific-skills.md index da8be28..c72ba54 100644 --- a/docs/scientific-skills.md +++ b/docs/scientific-skills.md @@ -169,6 +169,7 @@ - **HypoGeniC** - Automated hypothesis generation and testing using large language models to accelerate scientific discovery. Provides three frameworks: HypoGeniC (data-driven hypothesis generation from observational data), HypoRefine (synergistic approach combining literature insights with empirical patterns through an agentic system), and Union methods (mechanistic combination of literature and data-driven hypotheses). Features iterative refinement that improves hypotheses by learning from challenging examples, Redis caching for API cost reduction, and customizable YAML-based prompt templates. Includes command-line tools for generation (hypogenic_generation) and testing (hypogenic_inference). Research applications have demonstrated 14.19% accuracy improvement in AI-content detection and 7.44% in deception detection. Use cases: deception detection in reviews, AI-generated content identification, mental stress detection, exploratory research without existing literature, hypothesis-driven analysis in novel domains, and systematic exploration of competing explanations ### Scientific Communication & Publishing +- **pyzotero** - Python client for the Zotero Web API v3. Programmatically manage Zotero reference libraries: retrieve, create, update, and delete items, collections, tags, and attachments. Export citations as BibTeX, CSL-JSON, and formatted bibliography HTML. Supports user and group libraries, local mode for offline access, paginated retrieval with `everything()`, full-text content indexing, saved search management, and file upload/download. Includes a CLI for searching your local Zotero library. Use cases: building research automation pipelines that integrate with Zotero, bulk importing references, exporting bibliographies programmatically, managing large reference collections, syncing library metadata, and enriching bibliographic data. - **Citation Management** - Comprehensive citation management for academic research. Search Google Scholar and PubMed for papers, extract accurate metadata from multiple sources (CrossRef, PubMed, arXiv), validate citations, and generate properly formatted BibTeX entries. Features include converting DOIs, PMIDs, or arXiv IDs to BibTeX, cleaning and formatting bibliography files, finding highly cited papers, checking for duplicates, and ensuring consistent citation formatting. Use cases: building bibliographies for manuscripts, verifying citation accuracy, citation deduplication, and maintaining reference databases - **Generate Image** - AI-powered image generation and editing for scientific illustrations, schematics, and visualizations using OpenRouter's image generation models. Supports multiple models including google/gemini-3-pro-image-preview (high quality, recommended default) and black-forest-labs/flux.2-pro (fast, high quality). Key features include: text-to-image generation from detailed prompts, image editing capabilities (modify existing images with natural language instructions), automatic base64 encoding/decoding, PNG output with configurable paths, and comprehensive error handling. Requires OpenRouter API key (via .env file or environment variable). Use cases: generating scientific diagrams and illustrations, creating publication-quality figures, editing existing images (changing colors, adding elements, removing backgrounds), producing schematics for papers and presentations, visualizing experimental setups, creating graphical abstracts, and generating conceptual illustrations for scientific communication - **LaTeX Posters** - Create professional research posters in LaTeX using beamerposter, tikzposter, or baposter. Support for conference presentations, academic posters, and scientific communication with layout design, color schemes, multi-column formats, figure integration, and poster-specific best practices. Features compliance with conference size requirements (A0, A1, 36×48"), complex multi-column layouts, and integration of figures, tables, equations, and citations. Use cases: conference poster sessions, thesis defenses, symposia presentations, and research group templates diff --git a/scientific-skills/pyzotero/SKILL.md b/scientific-skills/pyzotero/SKILL.md new file mode 100644 index 0000000..6da1655 --- /dev/null +++ b/scientific-skills/pyzotero/SKILL.md @@ -0,0 +1,111 @@ +--- +name: pyzotero +description: Interact with Zotero reference management libraries using the pyzotero Python client. Retrieve, create, update, and delete items, collections, tags, and attachments via the Zotero Web API v3. Use this skill when working with Zotero libraries programmatically, managing bibliographic references, exporting citations, searching library contents, uploading PDF attachments, or building research automation workflows that integrate with Zotero. +allowed-tools: Read Write Edit Bash +license: MIT License +metadata: + skill-author: K-Dense Inc. +--- + +# Pyzotero + +Pyzotero is a Python wrapper for the [Zotero API v3](https://www.zotero.org/support/dev/web_api/v3/start). Use it to programmatically manage Zotero libraries: read items and collections, create and update references, upload attachments, manage tags, and export citations. + +## Authentication Setup + +**Required credentials** — get from https://www.zotero.org/settings/keys: +- **User ID**: shown as "Your userID for use in API calls" +- **API Key**: create at https://www.zotero.org/settings/keys/new +- **Library ID**: for group libraries, the integer after `/groups/` in the group URL + +Store credentials in environment variables or a `.env` file: +``` +ZOTERO_LIBRARY_ID=your_user_id +ZOTERO_API_KEY=your_api_key +ZOTERO_LIBRARY_TYPE=user # or "group" +``` + +See [references/authentication.md](references/authentication.md) for full setup details. + +## Installation + +```bash +uv add pyzotero +# or with CLI support: +uv add "pyzotero[cli]" +``` + +## Quick Start + +```python +from pyzotero import Zotero + +zot = Zotero(library_id='123456', library_type='user', api_key='ABC1234XYZ') + +# Retrieve top-level items (returns 100 by default) +items = zot.top(limit=10) +for item in items: + print(item['data']['title'], item['data']['itemType']) + +# Search by keyword +results = zot.items(q='machine learning', limit=20) + +# Retrieve all items (use everything() for complete results) +all_items = zot.everything(zot.items()) +``` + +## Core Concepts + +- A `Zotero` instance is bound to a single library (user or group). All methods operate on that library. +- Item data lives in `item['data']`. Access fields like `item['data']['title']`, `item['data']['creators']`. +- Pyzotero returns 100 items by default (API default is 25). Use `zot.everything(zot.items())` to get all items. +- Write methods return `True` on success or raise a `ZoteroError`. + +## Reference Files + +| File | Contents | +|------|----------| +| [references/authentication.md](references/authentication.md) | Credentials, library types, local mode | +| [references/read-api.md](references/read-api.md) | Retrieving items, collections, tags, groups | +| [references/search-params.md](references/search-params.md) | Filtering, sorting, search parameters | +| [references/write-api.md](references/write-api.md) | Creating, updating, deleting items | +| [references/collections.md](references/collections.md) | Collection CRUD operations | +| [references/tags.md](references/tags.md) | Tag retrieval and management | +| [references/files-attachments.md](references/files-attachments.md) | File retrieval and attachment uploads | +| [references/exports.md](references/exports.md) | BibTeX, CSL-JSON, bibliography export | +| [references/pagination.md](references/pagination.md) | follow(), everything(), generators | +| [references/full-text.md](references/full-text.md) | Full-text content indexing and retrieval | +| [references/saved-searches.md](references/saved-searches.md) | Saved search management | +| [references/cli.md](references/cli.md) | Command-line interface usage | +| [references/error-handling.md](references/error-handling.md) | Errors and exception handling | + +## Common Patterns + +### Fetch and modify an item +```python +item = zot.item('ITEMKEY') +item['data']['title'] = 'New Title' +zot.update_item(item) +``` + +### Create an item from a template +```python +template = zot.item_template('journalArticle') +template['title'] = 'My Paper' +template['creators'][0] = {'creatorType': 'author', 'firstName': 'Jane', 'lastName': 'Doe'} +zot.create_items([template]) +``` + +### Export as BibTeX +```python +zot.add_parameters(format='bibtex') +bibtex = zot.top(limit=50) +# bibtex is a bibtexparser BibDatabase object +print(bibtex.entries) +``` + +### Local mode (read-only, no API key needed) +```python +zot = Zotero(library_id='123456', library_type='user', local=True) +items = zot.items() +``` diff --git a/scientific-skills/pyzotero/references/authentication.md b/scientific-skills/pyzotero/references/authentication.md new file mode 100644 index 0000000..cd19f97 --- /dev/null +++ b/scientific-skills/pyzotero/references/authentication.md @@ -0,0 +1,90 @@ +# Authentication & Setup + +## Credentials + +Obtain from https://www.zotero.org/settings/keys: + +| Credential | Where to Find | +|-----------|---------------| +| **User ID** | "Your userID for use in API calls" section | +| **API Key** | Create new key at /settings/keys/new | +| **Group Library ID** | Integer after `/groups/` in group URL (e.g. `https://www.zotero.org/groups/169947`) | + +## Environment Variables + +Store in `.env` or export in shell: +``` +ZOTERO_LIBRARY_ID=436 +ZOTERO_API_KEY=ABC1234XYZ +ZOTERO_LIBRARY_TYPE=user +``` + +Load in Python: +```python +import os +from dotenv import load_dotenv +from pyzotero import Zotero + +load_dotenv() + +zot = Zotero( + library_id=os.environ['ZOTERO_LIBRARY_ID'], + library_type=os.environ['ZOTERO_LIBRARY_TYPE'], + api_key=os.environ['ZOTERO_API_KEY'] +) +``` + +## Library Types + +```python +# Personal library +zot = Zotero('436', 'user', 'ABC1234XYZ') + +# Group library +zot = Zotero('169947', 'group', 'ABC1234XYZ') +``` + +**Important**: A `Zotero` instance is bound to a single library. To access multiple libraries, create multiple instances. + +## Local Mode (Read-Only) + +Connect to your local Zotero installation without an API key. Only supports read requests. + +```python +zot = Zotero(library_id='436', library_type='user', local=True) +items = zot.items(limit=10) # reads from local Zotero +``` + +## Optional Parameters + +```python +zot = Zotero( + library_id='436', + library_type='user', + api_key='ABC1234XYZ', + preserve_json_order=True, # use OrderedDict for JSON responses + locale='en-US', # localise field names (e.g. 'fr-FR' for French) +) +``` + +## Key Permissions + +Check what the current API key can access: +```python +info = zot.key_info() +# Returns dict with user info and group access permissions +``` + +Check accessible groups: +```python +groups = zot.groups() +# Returns list of group libraries accessible to the current key +``` + +## API Key Scopes + +When creating an API key at https://www.zotero.org/settings/keys/new, choose appropriate permissions: +- **Read Only**: For retrieving items and collections +- **Write Access**: For creating, updating, and deleting items +- **Notes Access**: To include notes in read/write operations +- **Files Access**: Required for uploading attachments diff --git a/scientific-skills/pyzotero/references/cli.md b/scientific-skills/pyzotero/references/cli.md new file mode 100644 index 0000000..ba4a7d6 --- /dev/null +++ b/scientific-skills/pyzotero/references/cli.md @@ -0,0 +1,100 @@ +# Command-Line Interface + +The pyzotero CLI connects to your **local Zotero installation** (not the remote API). It requires a running local Zotero desktop app. + +## Installation + +```bash +uv add "pyzotero[cli]" +# or run without installing: +uvx --from "pyzotero[cli]" pyzotero search -q "your query" +``` + +## Searching + +```bash +# Search titles and metadata +pyzotero search -q "machine learning" + +# Full-text search (includes PDF content) +pyzotero search -q "climate change" --fulltext + +# Filter by item type +pyzotero search -q "methodology" --itemtype journalArticle --itemtype book + +# Filter by tags (AND logic) +pyzotero search -q "evolution" --tag "reviewed" --tag "high-priority" + +# Search within a collection +pyzotero search --collection ABC123 -q "test" + +# Paginate results +pyzotero search -q "deep learning" --limit 20 --offset 40 + +# Output as JSON (for machine processing) +pyzotero search -q "protein" --json +``` + +## Getting Individual Items + +```bash +# Get a single item by key +pyzotero item ABC123 + +# Get as JSON +pyzotero item ABC123 --json + +# Get child items (attachments, notes) +pyzotero children ABC123 --json + +# Get multiple items at once (up to 50) +pyzotero subset ABC123 DEF456 GHI789 --json +``` + +## Collections & Tags + +```bash +# List all collections +pyzotero listcollections + +# List all tags +pyzotero tags + +# Tags in a specific collection +pyzotero tags --collection ABC123 +``` + +## Full-Text Content + +```bash +# Get full-text content of an attachment +pyzotero fulltext ABC123 +``` + +## Item Types + +```bash +# List all available item types +pyzotero itemtypes +``` + +## DOI Index + +```bash +# Get complete DOI-to-key mapping (useful for caching) +pyzotero doiindex > doi_cache.json +# Returns JSON: {"10.1038/s41592-024-02233-6": {"key": "ABC123", "doi": "..."}} +``` + +## Output Format + +By default the CLI outputs human-readable text including title, authors, date, publication, volume, issue, DOI, URL, and PDF attachment paths. + +Use `--json` for structured JSON output suitable for piping to other tools. + +## Search Behaviour Notes + +- Default search covers top-level item titles and metadata fields only +- `--fulltext` expands search to PDF content; results show parent bibliographic items (not raw attachments) +- Multiple `--tag` flags use AND logic +- Multiple `--itemtype` flags use OR logic diff --git a/scientific-skills/pyzotero/references/collections.md b/scientific-skills/pyzotero/references/collections.md new file mode 100644 index 0000000..a631f20 --- /dev/null +++ b/scientific-skills/pyzotero/references/collections.md @@ -0,0 +1,113 @@ +# Collection Management + +## Reading Collections + +```python +# All collections (flat list including nested) +all_cols = zot.collections() + +# Only top-level collections +top_cols = zot.collections_top() + +# Specific collection +col = zot.collection('COLKEY') + +# Sub-collections of a collection +sub_cols = zot.collections_sub('COLKEY') + +# All collections under a given collection (recursive) +tree = zot.all_collections('COLKEY') +# Or all collections in the library: +tree = zot.all_collections() +``` + +## Collection Data Structure + +```python +col = zot.collection('5TSDXJG6') +name = col['data']['name'] +key = col['data']['key'] +parent = col['data']['parentCollection'] # False if top-level, else parent key +version = col['data']['version'] +n_items = col['meta']['numItems'] +n_sub_collections = col['meta']['numCollections'] +``` + +## Creating Collections + +```python +# Create a top-level collection +zot.create_collections([{'name': 'My New Collection'}]) + +# Create a nested collection +zot.create_collections([{ + 'name': 'Sub-Collection', + 'parentCollection': 'PARENTCOLKEY' +}]) + +# Create multiple at once +zot.create_collections([ + {'name': 'Collection A'}, + {'name': 'Collection B'}, + {'name': 'Sub-B', 'parentCollection': 'BKEY'}, +]) +``` + +## Updating Collections + +```python +cols = zot.collections() +# Rename the first collection +cols[0]['data']['name'] = 'Renamed Collection' +zot.update_collection(cols[0]) + +# Update multiple collections (auto-chunked at 50) +zot.update_collections(cols) +``` + +## Deleting Collections + +```python +# Delete a single collection +col = zot.collection('COLKEY') +zot.delete_collection(col) + +# Delete multiple collections +cols = zot.collections() +zot.delete_collection(cols) # pass a list of dicts +``` + +## Managing Items in Collections + +```python +# Add an item to a collection +item = zot.item('ITEMKEY') +zot.addto_collection('COLKEY', item) + +# Remove an item from a collection +zot.deletefrom_collection('COLKEY', item) + +# Get all items in a collection +items = zot.collection_items('COLKEY') + +# Get only top-level items in a collection +top_items = zot.collection_items_top('COLKEY') + +# Count items in a collection +n = zot.num_collectionitems('COLKEY') + +# Get tags in a collection +tags = zot.collection_tags('COLKEY') +``` + +## Find Collection Key by Name + +```python +def find_collection(zot, name): + for col in zot.everything(zot.collections()): + if col['data']['name'] == name: + return col['data']['key'] + return None + +key = find_collection(zot, 'Machine Learning Papers') +``` diff --git a/scientific-skills/pyzotero/references/error-handling.md b/scientific-skills/pyzotero/references/error-handling.md new file mode 100644 index 0000000..159896b --- /dev/null +++ b/scientific-skills/pyzotero/references/error-handling.md @@ -0,0 +1,103 @@ +# Error Handling + +## Exception Types + +Pyzotero raises `ZoteroError` subclasses for API errors. Import from `pyzotero.zotero_errors`: + +```python +from pyzotero import zotero_errors +``` + +Common exceptions: + +| Exception | Cause | +|-----------|-------| +| `UserNotAuthorised` | Invalid or missing API key | +| `HTTPError` | Generic HTTP error | +| `ParamNotPassed` | Required parameter missing | +| `CallDoesNotExist` | Invalid API method for library type | +| `ResourceNotFound` | Item/collection key not found | +| `Conflict` | Version conflict (optimistic locking) | +| `PreConditionFailed` | `If-Unmodified-Since-Version` check failed | +| `TooManyItems` | Batch exceeds 50-item limit | +| `TooManyRequests` | API rate limit exceeded | +| `InvalidItemFields` | Item dict contains unknown fields | + +## Basic Error Handling + +```python +from pyzotero import Zotero +from pyzotero import zotero_errors + +zot = Zotero('123456', 'user', 'APIKEY') + +try: + item = zot.item('BADKEY') +except zotero_errors.ResourceNotFound: + print('Item not found') +except zotero_errors.UserNotAuthorised: + print('Invalid API key') +except Exception as e: + print(f'Unexpected error: {e}') + if hasattr(e, '__cause__'): + print(f'Caused by: {e.__cause__}') +``` + +## Version Conflict Handling + +```python +try: + zot.update_item(item) +except zotero_errors.PreConditionFailed: + # Item was modified since you retrieved it — re-fetch and retry + fresh_item = zot.item(item['data']['key']) + fresh_item['data']['title'] = new_title + zot.update_item(fresh_item) +``` + +## Checking for Invalid Fields + +```python +from pyzotero import zotero_errors + +template = zot.item_template('journalArticle') +template['badField'] = 'bad value' + +try: + zot.check_items([template]) +except zotero_errors.InvalidItemFields as e: + print(f'Invalid fields: {e}') + # Fix fields before calling create_items +``` + +## Rate Limiting + +The Zotero API rate-limits requests. If you receive `TooManyRequests`: + +```python +import time +from pyzotero import zotero_errors + +def safe_request(func, *args, **kwargs): + retries = 3 + for attempt in range(retries): + try: + return func(*args, **kwargs) + except zotero_errors.TooManyRequests: + wait = 2 ** attempt + print(f'Rate limited, waiting {wait}s...') + time.sleep(wait) + raise RuntimeError('Max retries exceeded') + +items = safe_request(zot.items, limit=100) +``` + +## Accessing Underlying Error + +```python +try: + zot.item('BADKEY') +except Exception as e: + print(e.__cause__) # original HTTP error + print(e.__context__) # exception context +``` diff --git a/scientific-skills/pyzotero/references/exports.md b/scientific-skills/pyzotero/references/exports.md new file mode 100644 index 0000000..b7e246d --- /dev/null +++ b/scientific-skills/pyzotero/references/exports.md @@ -0,0 +1,102 @@ +# Export Formats + +## BibTeX + +```python +zot.add_parameters(format='bibtex') +bibtex_db = zot.top(limit=50) +# Returns a bibtexparser BibDatabase object + +# Access entries as list of dicts +entries = bibtex_db.entries +for entry in entries: + print(entry.get('title'), entry.get('author')) + +# Write to .bib file +import bibtexparser +with open('library.bib', 'w') as f: + bibtexparser.dump(bibtex_db, f) +``` + +## CSL-JSON + +```python +zot.add_parameters(content='csljson', limit=50) +csl_items = zot.items() +# Returns a list of dicts in CSL-JSON format +``` + +## Bibliography HTML (formatted citations) + +```python +# APA style bibliography +zot.add_parameters(content='bib', style='apa') +bib_entries = zot.items(limit=50) +# Returns list of HTML
My annotation here
' +zot.create_items([note_template], parentid='PARENTKEY') +``` + +## Updating Items + +```python +# Retrieve, modify, update +item = zot.item('ITEMKEY') +item['data']['title'] = 'Updated Title' +item['data']['abstractNote'] = 'New abstract text.' +success = zot.update_item(item) # returns True or raises error + +# Update many items at once (auto-chunked at 50) +items = zot.items(limit=10) +for item in items: + item['data']['extra'] += '\nProcessed' +zot.update_items(items) +``` + +## Deleting Items + +```python +# Must retrieve item first (version field is required) +item = zot.item('ITEMKEY') +zot.delete_item([item]) + +# Delete multiple items +items = zot.items(tag='to-delete') +zot.delete_item(items) +``` + +## Item Types and Fields + +```python +# All available item types +item_types = zot.item_types() +# [{'itemType': 'artwork', 'localized': 'Artwork'}, ...] + +# All available fields +fields = zot.item_fields() + +# Valid fields for a specific item type +journal_fields = zot.item_type_fields('journalArticle') + +# Valid creator types for an item type +creator_types = zot.item_creator_types('journalArticle') +# [{'creatorType': 'author', 'localized': 'Author'}, ...] + +# All localised creator field names +creator_fields = zot.creator_fields() + +# Attachment link modes (needed for attachment templates) +link_modes = zot.item_attachment_link_modes() + +# Template for an attachment +attach_template = zot.item_template('attachment', linkmode='imported_file') +``` + +## Optimistic Locking + +Use `last_modified` to prevent overwriting concurrent changes: + +```python +# Only update if library version matches +zot.update_item(item, last_modified=4025) +# Raises an error if the server version differs +``` + +## Notes + +- `create_items()` accepts up to 50 items per call; batch if needed. +- `update_items()` auto-chunks at 50 items. +- If a dict passed to `create_items()` contains a `key` matching an existing item, it will be updated rather than created. +- Always call `check_items()` before `create_items()` to catch field errors early.