Compare commits

..

2 Commits

7 changed files with 172 additions and 775 deletions

View File

@@ -1,96 +0,0 @@
# YouTrack API References
## Core API Endpoints
### Base URL
Your YouTrack instance: `https://your-instance.youtrack.cloud/`
API base: `https://your-instance.youtrack.cloud/api/`
### Authentication
- Use permanent token with `Authorization: Bearer <token>` header
- To generate a token: From main navigation menu, select **Administration** > **Access Management** > **Users**, find your user, and generate a permanent API token
### Projects
**List all projects**
- `GET /api/admin/projects?fields=id,name,shortName`
- Returns array of project objects with id, name, shortName
**Get specific project**
- `GET /api/admin/projects/{projectId}?fields=id,name,shortName,description`
### Issues
**List issues with query**
- `GET /api/issues?query={query}&fields={fields}`
- Query examples:
- `project: MyProject`
- `project: MyProject updated: 2026-01-01 ..`
- `assignee: me`
- Common fields to request:
- `id,summary,description,created,updated,project(id,name),customFields(name,value)`
**Get issue**
- `GET /api/issues/{issueId}`
**Create issue**
- `POST /api/issues`
- Body: `{"project": {"id": "projectId"}, "summary": "Summary", "description": "Description"}`
**Update issue**
- `POST /api/issues/{issueId}`
- Body: `{"summary": "New summary", "description": "New description"}`
### Time Tracking
**Get work items for an issue**
- `GET /api/issues/{issueId}/timeTracking/workItems`
- Returns array of work items with:
- `id`, `date`, `duration` (in minutes), `author`, `text`
**Work item structure:**
```json
{
"id": "...",
"date": "2026-01-15T10:30:00.000+0000",
"duration": {"minutes": 30},
"author": {"name": "User Name", "id": "..."},
"text": "Work description",
"type": {...}
}
```
### Knowledge Base (Articles)
**List articles**
- `GET /api/articles?project={projectId}`
**Get article**
- `GET /api/articles/{articleId}`
**Create article**
- `POST /api/articles`
- Body: `{"project": {"id": "projectId"}, "title": "Title", "content": "Content"}`
## Query Language Examples
```
project: MyProject
project: MyProject assignee: me
project: MyProject updated >= 2026-01-01
priority: Critical
has: time
```
**Note:** Date filtering in REST API uses `updated >= YYYY-MM-DD` format, not `updated: date ..` format.
## Field IDs Reference
Common custom field IDs (may vary by setup):
- Priority: `priority`
- State: `State`
- Assignee: `Assignee`
- Due Date: `Due Date`
- Type: `Type`
Check your instance's actual field IDs in YouTrack UI or via API.

View File

@@ -1,218 +1,104 @@
--- ---
name: youtrack name: youtrack
description: Interact with YouTrack project management system via REST API. Read projects and issues, create tasks, generate invoices from time tracking data, and manage knowledge base articles. Use for reading projects and work items, creating or updating issues, generating client invoices from time tracking, and working with knowledge base articles. description: Dynamic access to youtrack MCP server (19 tools)
user-invocable: false
disable-model-invocation: false
--- ---
# YouTrack # youtrack Skill
YouTrack integration for project management, time tracking, and knowledge base. This skill provides dynamic access to the youtrack MCP server without loading all tool definitions into context.
## Quick Start ## Available Tools
### Authentication - `log_work`: Adds a work item (spent time) to the specified issue. You can specify the duration (in minutes), optional date, work type, description, and optional work item attributes. Use get_project to retrieve the workTypes and workItemAttributesSchema for the target project.
- `manage_issue_tags`: Adds a tag to or removes a tag from an issue. If a name is used, the first tag that matches the provided name is added. If no matching tags are found, an error message with suggestions for similar tags is returned. When successful, it returns the ID of the updated issue and the updated list of issue tags.
- `search_issues`: Searches for issues using YouTracks query language. The 'query' can combine attribute filters, keywords, and free text. Examples of common patterns include:
To generate a permanent token: - Free text: Find matching words in the issue summary, description, and comments. Use wildcards: '*' for any characters, '?' for single characters (e.g., 'summary: log*', 'fix??'). Examples: 'login button bug', 'some other text', 'summary: log*', 'description: fix??'.
1. From the main navigation menu, select **Administration** > **Access Management** > **Users** - Linked issues: '<linkType>: <issueId>' (by link type), 'links: <issueId>' (all linked to issueId issues). Examples: 'relates to: DEMO-123', 'subtask of: DEMO-123' (issues where DEMO-123 is a parent), 'links: DEMO-12' (issues linked to DEMO-12 with any link type). Hint: get_issue returns 'linkedIssueCounts' property which shows the available link types for the issue.
2. Find your user and click to open settings - Issues where an issue is mentioned: 'mentions: <issueId>'. Examples: 'mentions: DEMO-123'.
3. Generate a new permanent API token - Project filter: 'project: <ProjectName>'. Use project name or project key. Examples: 'project: {Mobile App}', 'project: MA'.
4. Set the token as an environment variable: - Assignee filter: 'for: <login>'. Use 'me' for the currently authenticated user. Examples: 'for: me', 'for: john.smith'.
- Reporter filter: 'reporter: <login>'. Use 'me' for the currently authenticated user. Examples: 'reporter: me', 'reporter: admin'.
- Tag filter: 'tag: <TagName>'. Wrap multi-word tags in braces { }. Examples: 'tag: urgent', 'tag: {customer feedback}'.
- Field filter: '<FieldName>: <Value>'. For any project field, for example, State, Type, Priority, and so on. Wrap multi-word names or values in { }. Use get_project to get the possible fields and values for the project issues to search. Use '-' as 'not', e.g., 'State: -Fixed' filters out fixed issues. Examples: 'Priority: High', 'State: {In Progress}, Fixed' (searches issues with 'In Progress' state + issues with 'Fixed' state), 'Due Date: {plus 5d}' (issues that are due in five days).
- Date filters: 'created:', 'updated:', 'resolved date:' (or any date field) plus a date, range, or relative period. Relative periods: 'today', 'yesterday', '{This week}', '{Last week}', '{This month}', etc. Examples: 'created: {This month}', 'updated: today', 'resolved date: 2025-06-01 .. 2025-06-30', 'updated: {minus 2h} .. *' (issues updated last 2 hours), 'created: * .. {minus 1y 6M}' (issues that are at least one and a half years old).
- Keywords: '#Unresolved' to find unresolved issues based on the State; '#Resolved' to find resolved issues.
- Empty/Non-Empty Fields: Use the 'has: <attribute>'. Example: 'has: attachments' finds issues with attachments, while 'has: -comments' finds issues with no comments. Other attributes: 'links', '<linkType>' (e.g. 'has: {subtask of}'), 'star' (subscription), 'votes', 'work'.
- Combining filters: List multiple conditions separated by spaces (logical AND). For OR operator, add it explicitly. Examples: '(project: MA) and (for: me) and (created: {minus 8h} .. *) and runtime error' (issues in project MA and assigned to currently authenticated user and created during last 8h and contains 'runtime error' text), '(Type: Task and State: Open) or (Type: Bug and Priority: Critical)'.
Returns basic info: id, summary, project, resolved, reporter, created, updated and default custom fields. For full details, use get_issue. The response is paginated using the specified offset and limit.
- `update_issue`: Updates an existing issue and its fields (customFields). Pass any of the arguments to partially update the issue:
- 'summary' or 'description' arguments to update only the issue summary or description.
- 'customFields' argument as key-value JSON object to update issue fields like State, Type, Priority, etc. Use get_issue_fields_schema to discover 'customFields' and their possible values.
- 'subscription' argument to star (true) or unstar (false) the issue on behalf of the current user. The current user is notified about subsequent issue updates according to their subscription settings for the Star tag.
- 'vote' argument to vote (true) or remove a vote (false) on behalf of the current user for the issue.
Returns the ID of the updated issue and the confirmation what was updated.
- `get_project`: Retrieves full details for a specific project.
- `get_saved_issue_searches`: Returns saved searches marked as favorites by the current user. The output search queries can be used in search_issues. The response is paginated using the specified offset and/or limit.
- `get_user_group_members`: Lists users who are members of a specified group or project team. Project teams are essentially groups that are always associated with a specific project. The response is paginated using the specified offset and/or limit.
- `link_issues`: Links two issues with the specified link type.
Examples:
- TS-1 is a subtask of TS-2: {"targetIssueId": "TS-1", "linkType": "subtask of", "issueToLinkId": "TS-2"};
- TS-4 is a duplicate of TS-3: {"targetIssueId": "TS-4", "linkType": "duplicates", "issueToLinkId": "TS-3"};
- TS-1 is blocked by TS-2: {"targetIssueId": "TS-1", "linkType": "blocked by", "issueToLinkId": "TS-2"};
Returns updated link counts for all target issue link types.
- `get_current_user`: Returns details about the currently authenticated user (me): login, email, full name, time zone.
- `get_issue`: Returns detailed information for an issue or issue draft, including the summary, description, URL, project, reporter (login), tags, votes, and custom fields. The `customFields` output property provides more important issue details, including Type, State, Assignee, Priority, Subsystem, and so on. Use get_issue_fields_schema for the full list of custom fields and their possible values.
- `get_issue_comments`: Returns a list of issue comments with detailed information for each. The response is paginated using the specified offset and/or limit
- `get_issue_fields_schema`: Returns the JSON schema for custom fields in the specified project. Must be used to provide relevant custom fields and values for create_issue and update_issue actions.
- `find_projects`: Finds projects whose names contain the specified substring (case-insensitive). Returns minimal information (ID and name) to help pick a project for get_project. The response is paginated using the specified offset and/or limit.
- `find_user`: Finds users by login or email (provide either login or email). Returns profile data for the matching user. This includes the login, full name, email, and local time zone.
- `find_user_groups`: Finds user groups or project teams whose names contain the specified substring (case-insensitive). The response is paginated using the specified offset and/or limit.
- `add_issue_comment`: Adds a new comment to the specified issue. Supports Markdown.
- `change_issue_assignee`: Sets the value for the Assignee field in an issue to the specified user. If the `assigneeLogin` argument is `null`, the issue will be unassigned.
- `create_draft_issue`: Creates a new issue draft in the specified project. If project is not defined, ask for assistance. Draft issues are only visible to the current user and can be edited using update_issue. Returns the ID assigned to the issue draft and a URL that opens the draft in a web browser.
- `create_issue`: Creates a new issue in the specified project. Call the get_issue_fields_schema tool first to identify required `customFields` and permitted values (projects may require them at creation). If project is not defined, ask for assistance. Returns the created issue ID and URL. Use get_issue for full details.
## Usage Pattern
When the user's request matches this skill's capabilities:
**Step 1: Identify the right tool** from the list above
**Step 2: Generate a tool call** in this JSON format:
```json
{
"tool": "tool_name",
"arguments": {
"param1": "value1"
}
}
```
**Step 3: Execute via bash:**
```bash ```bash
export YOUTRACK_TOKEN=your-permanent-token-here cd $SKILL_DIR
./executor.py --call 'YOUR_JSON_HERE'
``` ```
**Important:** Configure your hourly rate (default $100/hour) by passing `--rate` to invoice_generator.py or updating `hourly_rate` parameter in your code. IMPORTANT: Replace $SKILL_DIR with the actual discovered path of this skill directory.
Then use any YouTrack script: ## Getting Tool Details
If you need detailed information about a specific tool's parameters:
```bash ```bash
# List all projects cd $SKILL_DIR
python3 scripts/youtrack_api.py --url https://your-instance.youtrack.cloud --list-projects ./executor.py --describe tool_name
# List issues in a project
python3 scripts/youtrack_api.py --url https://your-instance.youtrack.cloud --list-issues "project: MyProject"
# Generate invoice for a project
python3 scripts/invoice_generator.py --url https://your-instance.youtrack.cloud --project MyProject --month "January 2026" --from-date "2026-01-01"
``` ```
## Python Scripts
### `scripts/youtrack_api.py`
Core API client for all YouTrack operations.
**In your Python code:**
```python
from youtrack_api import YouTrackAPI
api = YouTrackAPI('https://your-instance.youtrack.cloud', token='your-token')
# Projects
projects = api.get_projects()
project = api.get_project('project-id')
# Issues
issues = api.get_issues(query='project: MyProject')
issue = api.get_issue('issue-id')
# Create issue
api.create_issue('project-id', 'Summary', 'Description')
# Work items (time tracking)
work_items = api.get_work_items('issue-id')
issue_with_time = api.get_issue_with_work_items('issue-id')
# Knowledge base
articles = api.get_articles()
article = api.get_article('article-id')
api.create_article('project-id', 'Title', 'Content')
```
**CLI usage:**
```bash
python3 scripts/youtrack_api.py --url https://your-instance.youtrack.cloud \
--token YOUR_TOKEN \
--list-projects
python3 scripts/youtrack_api.py --url https://your-instance.youtrack.cloud \
--get-issue ABC-123
python3 scripts/youtrack_api.py --url https://your-instance.youtrack.cloud \
--get-articles
```
### `scripts/invoice_generator.py`
Generate client invoices from time tracking data.
**In your Python code:**
```python
from youtrack_api import YouTrackAPI
from invoice_generator import InvoiceGenerator
api = YouTrackAPI('https://your-instance.youtrack.cloud', token='your-token')
generator = InvoiceGenerator(api, hourly_rate=100.0)
# Get time data for a project
project_data = generator.get_project_time_data('project-id', from_date='2026-01-01')
# Generate invoice
invoice_text = generator.generate_invoice_text(project_data, month='January 2026')
print(invoice_text)
```
**CLI usage:**
```bash
python3 scripts/invoice_generator.py \
--url https://your-instance.youtrack.cloud \
--project MyProject \
--from-date 2026-01-01 \
--month "January 2026" \
--rate 100 \
--format text
```
Save the text output and print to PDF for clients.
## Common Workflows
### 1. List All Projects
```bash
python3 scripts/youtrack_api.py --url https://your-instance.youtrack.cloud --list-projects
```
### 2. Find Issues in a Project
```bash
# All issues in a project
python3 scripts/youtrack_api.py --url https://your-instance.youtrack.cloud --list-issues "project: MyProject"
# Issues updated since a date
python3 scripts/youtrack_api.py --url https://your-instance.youtrack.cloud --list-issues "project: MyProject updated >= 2026-01-01"
# Issues assigned to you
python3 scripts/youtrack_api.py --url https://your-instance.youtrack.cloud --list-issues "assignee: me"
```
### 3. Create a New Issue
```python
from youtrack_api import YouTrackAPI
api = YouTrackAPI('https://your-instance.youtrack.cloud')
api.create_issue(
project_id='MyProject',
summary='Task title',
description='Task description'
)
```
### 4. Generate Monthly Invoice
```bash
# Generate invoice for January 2026
python3 scripts/invoice_generator.py \
--url https://your-instance.youtrack.cloud \
--project ClientProject \
--from-date 2026-01-01 \
--month "January 2026" \
--rate 100 \
--format text > invoice.txt
```
Save the text output and print to PDF for clients.
### 5. Read Knowledge Base
```python
from youtrack_api import YouTrackAPI
api = YouTrackAPI('https://your-instance.youtrack.cloud')
# All articles
articles = api.get_articles()
# Articles for specific project
articles = api.get_articles(project_id='MyProject')
# Get specific article
article = api.get_article('article-id')
```
## Billing Logic
Invoice generator uses this calculation:
1. Sum all time tracked per issue (in minutes)
2. Convert to 30-minute increments (round up)
3. Minimum charge is 30 minutes (at configured rate/2)
4. Multiply by rate (default $100/hour = $50 per half-hour)
Examples:
- 15 minutes → $50 (30 min minimum)
- 35 minutes → $100 (rounded to 60 min)
- 60 minutes → $100
- 67 minutes → $150 (rounded to 90 min)
## Environment Variables
- `YOUTRACK_TOKEN`: Your permanent API token (recommended over passing as argument)
- Set with `export YOUTRACK_TOKEN=your-token`
## API Details
See `REFERENCES.md` for:
- Complete API endpoint documentation
- Query language examples
- Field IDs and structures
## Error Handling ## Error Handling
Scripts will raise errors for: If the executor returns an error:
- Missing or invalid token - Check the tool name is correct
- Network issues - Verify required arguments are provided
- API errors (404, 403, etc.) - Ensure the MCP server is accessible
Check stderr for error details. ---
*Auto-generated from MCP server configuration by mcp_to_skill.py*

View File

@@ -1,11 +0,0 @@
{
"owner": "digisal",
"slug": "youtrack-digisal",
"displayName": "YouTrack Project Management",
"latest": {
"version": "1.0.1",
"publishedAt": 1769572337862,
"commit": "https://github.com/clawdbot/skills/commit/62ea9dc7577ed1e4c2a99c7d470bc5afbc58d356"
},
"history": []
}

View File

@@ -0,0 +1,81 @@
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.10"
# dependencies = [
# "mcp>=1.0.0",
# "httpx",
# ]
# ///
"""MCP Skill Executor - HTTP (Streamable HTTP) transport"""
import json
import sys
import asyncio
import argparse
from pathlib import Path
import httpx
from mcp import ClientSession
from mcp.client.streamable_http import streamable_http_client
async def run(config, args):
url = config["url"]
headers = config.get("headers", {})
http_client = httpx.AsyncClient(headers=headers, timeout=httpx.Timeout(30, read=60))
async with http_client:
async with streamable_http_client(url=url, http_client=http_client) as (
read_stream,
write_stream,
_,
):
async with ClientSession(read_stream, write_stream) as session:
await session.initialize()
if args.list:
response = await session.list_tools()
tools = [{"name": t.name, "description": t.description} for t in response.tools]
print(json.dumps(tools, indent=2))
elif args.describe:
response = await session.list_tools()
for tool in response.tools:
if tool.name == args.describe:
print(json.dumps({"name": tool.name, "description": tool.description, "inputSchema": tool.inputSchema}, indent=2))
return
print(f"Tool not found: {args.describe}", file=sys.stderr)
sys.exit(1)
elif args.call:
call_data = json.loads(args.call)
result = await session.call_tool(call_data["tool"], call_data.get("arguments", {}))
for item in result.content:
if hasattr(item, "text"):
print(item.text)
else:
print(json.dumps(item.model_dump(), indent=2))
else:
parser.print_help()
def main():
parser = argparse.ArgumentParser(description="MCP Skill Executor (HTTP)")
parser.add_argument("--call", help="JSON tool call to execute")
parser.add_argument("--describe", help="Get tool schema")
parser.add_argument("--list", action="store_true", help="List all tools")
args = parser.parse_args()
config_path = Path(__file__).parent / "mcp-config.json"
if not config_path.exists():
print(f"Error: {config_path} not found", file=sys.stderr)
sys.exit(1)
with open(config_path) as f:
config = json.load(f)
asyncio.run(run(config, args))
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,7 @@
{
"url": "https://<YourYouTrackInstance>.youtrack.cloud/mcp",
"transport": "http",
"headers": {
"Authorization": "Bearer <YourYouTrackToken>"
}
}

View File

@@ -1,221 +0,0 @@
#!/usr/bin/env python3
"""
YouTrack Invoice Generator
Generates invoices from time tracking data in YouTrack projects.
"""
import os
import sys
import json
import argparse
from datetime import datetime, timedelta
from typing import List, Dict, Any, Optional
from youtrack_api import YouTrackAPI
class InvoiceGenerator:
"""Generate invoices from YouTrack time tracking data."""
def __init__(self, api: YouTrackAPI, hourly_rate: float = 100.0):
"""
Initialize invoice generator.
Args:
api: YouTrackAPI instance
hourly_rate: Rate per hour (default $100)
"""
self.api = api
self.hourly_rate = hourly_rate
self.rate_per_half_hour = hourly_rate / 2
def get_project_time_data(self, project_id: str, from_date: Optional[str] = None) -> Dict[str, Any]:
"""
Get time tracking data for a project.
Args:
project_id: Project ID
from_date: Optional start date (not currently supported in REST API)
Returns:
Dictionary with time data per issue
"""
# Build query for project issues
# Note: Date filtering with 'updated:' syntax not supported in REST API
# Use 'updated >= YYYY-MM-DD' format instead
query = f'project: {project_id}'
if from_date:
query += f' updated >= {from_date}'
issues = self.api.get_issues(query=query)
project_data = {
'project': None,
'issues': [],
'total_minutes': 0,
'total_hours': 0,
'total_cost': 0
}
# Get project info
try:
project_data['project'] = self.api.get_project(project_id)
except:
# Fallback: use project name from first issue
if issues:
project_data['project'] = {'name': issues[0].get('project', {}).get('name', project_id)}
for issue in issues:
issue_id = issue.get('id')
issue_with_time = self.api.get_issue_with_work_items(issue_id)
work_items = issue_with_time.get('workItems', [])
total_minutes = sum(
wi.get('duration', {}).get('minutes', 0)
for wi in work_items
)
if total_minutes > 0:
hours = total_minutes / 60
cost = self._calculate_cost(total_minutes)
project_data['issues'].append({
'id': issue_id,
'summary': issue.get('summary', 'No summary'),
'description': issue.get('description', ''),
'work_items': work_items,
'total_minutes': total_minutes,
'total_hours': hours,
'cost': cost
})
project_data['total_minutes'] += total_minutes
project_data['total_cost'] += cost
project_data['total_hours'] = project_data['total_minutes'] / 60
return project_data
def _calculate_cost(self, minutes: int) -> float:
"""
Calculate cost based on time, billed in 30-minute increments.
Args:
minutes: Total minutes
Returns:
Total cost
"""
# Convert to 30-minute increments, round up
half_hour_units = (minutes + 29) // 30
# Minimum 30 minutes (1 half-hour unit)
half_hour_units = max(half_hour_units, 1)
return half_hour_units * self.rate_per_half_hour
def generate_invoice_text(self, project_data: Dict[str, Any], month: Optional[str] = None) -> str:
"""
Generate invoice as plain text (can be printed to PDF).
Args:
project_data: Project data from get_project_time_data()
month: Optional month label (e.g., "January 2026")
Returns:
Invoice text
"""
project = project_data['project'] or {}
project_name = project.get('name', 'Unknown Project')
lines = []
lines.append("=" * 70)
lines.append(f"INVOICE - {project_name}")
if month:
lines.append(f"Period: {month}")
lines.append("")
lines.append("WORK ITEMS")
lines.append("-" * 70)
for issue in project_data['issues']:
lines.append("")
lines.append(f"Task: {issue['summary']}")
lines.append(f"ID: {issue['id']}")
if issue['description']:
desc = issue['description'][:200] + "..." if len(issue['description']) > 200 else issue['description']
lines.append(f"Description: {desc}")
for wi in issue['work_items']:
duration = wi.get('duration', {})
mins = duration.get('minutes', 0)
date = wi.get('date', '')
author = wi.get('author', {}).get('name', 'Unknown')
lines.append(f" - {date}: {mins} min ({author})")
lines.append(f" Task total: {issue['total_hours']:.2f} hours (${issue['cost']:.2f})")
lines.append("")
lines.append("-" * 70)
lines.append(f"TOTAL: {project_data['total_hours']:.2f} hours")
lines.append(f"TOTAL COST: ${project_data['total_cost']:.2f}")
lines.append("=" * 70)
return "\n".join(lines)
def generate_invoice_json(self, project_data: Dict[str, Any], month: Optional[str] = None) -> str:
"""
Generate invoice as JSON for programmatic use.
Args:
project_data: Project data from get_project_time_data()
month: Optional month label
Returns:
JSON string
"""
invoice = {
'project': project_data['project'],
'period': month,
'items': project_data['issues'],
'summary': {
'total_minutes': project_data['total_minutes'],
'total_hours': project_data['total_hours'],
'total_cost': project_data['total_cost']
}
}
return json.dumps(invoice, indent=2)
def main():
"""CLI interface for generating invoices."""
parser = argparse.ArgumentParser(description='YouTrack Invoice Generator')
parser.add_argument('--url', required=True, help='YouTrack instance URL')
parser.add_argument('--token', help='API token (or set YOUTRACK_TOKEN env var)')
parser.add_argument('--project', required=True, help='Project ID to generate invoice for')
parser.add_argument('--from-date', help='Start date (YYYY-MM-DD)')
parser.add_argument('--month', help='Month label (e.g., "January 2026")')
parser.add_argument('--rate', type=float, default=100.0, help='Hourly rate (default: 100)')
parser.add_argument('--format', choices=['text', 'json'], default='text', help='Output format')
args = parser.parse_args()
try:
api = YouTrackAPI(args.url, args.token)
generator = InvoiceGenerator(api, hourly_rate=args.rate)
project_data = generator.get_project_time_data(args.project, args.from_date)
if args.format == 'text':
output = generator.generate_invoice_text(project_data, args.month)
else:
output = generator.generate_invoice_json(project_data, args.month)
print(output)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == '__main__':
main()

View File

@@ -1,249 +0,0 @@
#!/usr/bin/env python3
"""
YouTrack REST API Client
Handles authentication and basic API calls for YouTrack Cloud instances.
"""
import os
import sys
import json
import argparse
from urllib.parse import urljoin
from typing import Optional, Dict, List, Any
import urllib.request
import urllib.error
from datetime import datetime
class YouTrackAPI:
"""Simple YouTrack REST API client."""
def __init__(self, base_url: str, token: Optional[str] = None):
"""
Initialize YouTrack API client.
Args:
base_url: Your YouTrack instance URL (e.g., https://sl.youtrack.cloud)
token: Permanent API token (or set YOUTRACK_TOKEN env var)
"""
# Normalize base URL
self.base_url = base_url.rstrip('/')
self.token = token or os.environ.get('YOUTRACK_TOKEN')
if not self.token:
raise ValueError(
"YouTrack token required. Set YOUTRACK_TOKEN env var or pass as argument."
)
# Set up headers with bearer token auth
self.headers = {
'Authorization': f'Bearer {self.token}',
'Accept': 'application/json',
'Content-Type': 'application/json'
}
def _make_request(self, method: str, endpoint: str, data: Optional[Dict] = None) -> Dict[str, Any]:
"""
Make an authenticated API request.
Args:
method: HTTP method (GET, POST, PUT, DELETE)
endpoint: API endpoint (e.g., '/api/issues')
data: Request body for POST/PUT
Returns:
Parsed JSON response
"""
url = urljoin(self.base_url, endpoint)
req_data = None
if data is not None:
req_data = json.dumps(data).encode('utf-8')
req = urllib.request.Request(
url,
data=req_data,
headers=self.headers,
method=method
)
try:
with urllib.request.urlopen(req) as response:
if response.status >= 400:
error_body = response.read().decode('utf-8')
raise RuntimeError(f"API Error {response.status}: {error_body}")
result = response.read().decode('utf-8')
if result:
return json.loads(result)
return {}
except urllib.error.HTTPError as e:
error_body = e.read().decode('utf-8') if e.fp else ''
raise RuntimeError(f"HTTP Error {e.code}: {error_body}")
except Exception as e:
raise RuntimeError(f"Request failed: {e}")
# Projects
def get_projects(self) -> List[Dict]:
"""Get all projects."""
result = self._make_request('GET', '/api/admin/projects?fields=id,name,shortName')
return result if isinstance(result, list) else []
def get_project(self, project_id: str) -> Dict:
"""Get a specific project by ID."""
return self._make_request('GET', f'/api/admin/projects/{project_id}?fields=id,name,shortName,description')
# Issues
def get_issues(self, query: Optional[str] = None, fields: str = 'id,summary,description,created,updated,project(id,name),customFields(name,value)') -> List[Dict]:
"""
Get issues, optionally filtered by a query.
Args:
query: YouTrack query language (e.g., 'project: MyProject')
fields: Comma-separated list of fields to return
Returns:
List of issues
"""
params = {'fields': fields}
if query:
params['query'] = query
# Build query string
query_string = '&'.join(f'{k}={urllib.parse.quote(str(v))}' for k, v in params.items())
endpoint = f'/api/issues?{query_string}'
result = self._make_request('GET', endpoint)
return result if isinstance(result, list) else []
def get_issue(self, issue_id: str) -> Dict:
"""Get a specific issue by ID."""
return self._make_request('GET', f'/api/issues/{issue_id}')
def create_issue(self, project_id: str, summary: str, description: str = '') -> Dict:
"""
Create a new issue.
Args:
project_id: Project ID or short name
summary: Issue summary
description: Issue description
Returns:
Created issue
"""
data = {
'project': {'id': project_id},
'summary': summary,
'description': description
}
return self._make_request('POST', '/api/issues', data)
def update_issue(self, issue_id: str, summary: Optional[str] = None, description: Optional[str] = None) -> Dict:
"""Update an issue's summary and/or description."""
data = {}
if summary is not None:
data['summary'] = summary
if description is not None:
data['description'] = description
return self._make_request('POST', f'/api/issues/{issue_id}', data)
# Time Tracking
def get_work_items(self, issue_id: str) -> List[Dict]:
"""Get all work items (time entries) for an issue."""
result = self._make_request('GET', f'/api/issues/{issue_id}/timeTracking/workItems?fields=id,date,duration(minutes),author(name),text')
# Convert date from milliseconds to ISO format
for wi in result:
if 'date' in wi and wi['date']:
wi['date'] = datetime.fromtimestamp(wi['date'] / 1000).isoformat()
return result if isinstance(result, list) else []
def get_issue_with_work_items(self, issue_id: str) -> Dict:
"""Get an issue with all its work items included."""
issue = self.get_issue(issue_id)
work_items = self.get_work_items(issue_id)
issue['workItems'] = work_items
return issue
# Knowledge Base (Articles)
def get_articles(self, project_id: Optional[str] = None) -> List[Dict]:
"""
Get knowledge base articles.
Args:
project_id: Optional project ID to filter by
Returns:
List of articles
"""
endpoint = '/api/articles'
if project_id:
endpoint += f'?project={project_id}'
result = self._make_request('GET', endpoint)
return result if isinstance(result, list) else []
def get_article(self, article_id: str) -> Dict:
"""Get a specific article by ID."""
return self._make_request('GET', f'/api/articles/{article_id}')
def create_article(self, project_id: str, title: str, content: str) -> Dict:
"""
Create a new knowledge base article.
Args:
project_id: Project ID
title: Article title
content: Article content
Returns:
Created article
"""
data = {
'project': {'id': project_id},
'title': title,
'content': content
}
return self._make_request('POST', '/api/articles', data)
def main():
"""CLI interface for testing the YouTrack API."""
parser = argparse.ArgumentParser(description='YouTrack API Client')
parser.add_argument('--url', required=True, help='YouTrack instance URL')
parser.add_argument('--token', help='API token (or set YOUTRACK_TOKEN env var)')
parser.add_argument('--list-projects', action='store_true', help='List all projects')
parser.add_argument('--list-issues', help='List issues (optional query)')
parser.add_argument('--get-issue', help='Get specific issue ID')
parser.add_argument('--get-articles', action='store_true', help='List articles')
args = parser.parse_args()
try:
api = YouTrackAPI(args.url, args.token)
if args.list_projects:
projects = api.get_projects()
print(json.dumps(projects, indent=2))
elif args.list_issues is not None:
issues = api.get_issues(query=args.list_issues)
print(json.dumps(issues, indent=2))
elif args.get_issue:
issue = api.get_issue_with_work_items(args.get_issue)
print(json.dumps(issue, indent=2))
elif args.get_articles:
articles = api.get_articles()
print(json.dumps(articles, indent=2))
else:
print("No action specified. Use --help for options.")
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == '__main__':
main()