chore: remove YouTrack integration files and documentation
This commit is contained in:
@@ -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.
|
||||
@@ -1,218 +0,0 @@
|
||||
---
|
||||
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.
|
||||
---
|
||||
|
||||
# YouTrack
|
||||
|
||||
YouTrack integration for project management, time tracking, and knowledge base.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Authentication
|
||||
|
||||
To generate a permanent token:
|
||||
1. From the main navigation menu, select **Administration** > **Access Management** > **Users**
|
||||
2. Find your user and click to open settings
|
||||
3. Generate a new permanent API token
|
||||
4. Set the token as an environment variable:
|
||||
|
||||
```bash
|
||||
export YOUTRACK_TOKEN=your-permanent-token-here
|
||||
```
|
||||
|
||||
**Important:** Configure your hourly rate (default $100/hour) by passing `--rate` to invoice_generator.py or updating `hourly_rate` parameter in your code.
|
||||
|
||||
Then use any YouTrack script:
|
||||
|
||||
```bash
|
||||
# List all projects
|
||||
python3 scripts/youtrack_api.py --url https://your-instance.youtrack.cloud --list-projects
|
||||
|
||||
# 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
|
||||
|
||||
Scripts will raise errors for:
|
||||
- Missing or invalid token
|
||||
- Network issues
|
||||
- API errors (404, 403, etc.)
|
||||
|
||||
Check stderr for error details.
|
||||
@@ -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": []
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user