migrated all pawprint work
This commit is contained in:
0
artery/veins/jira/core/__init__.py
Normal file
0
artery/veins/jira/core/__init__.py
Normal file
37
artery/veins/jira/core/auth.py
Normal file
37
artery/veins/jira/core/auth.py
Normal file
@@ -0,0 +1,37 @@
|
||||
"""
|
||||
Jira credentials authentication for Jira vein.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from fastapi import Header, HTTPException
|
||||
from .config import settings
|
||||
|
||||
|
||||
@dataclass
|
||||
class JiraCredentials:
|
||||
email: str
|
||||
token: str
|
||||
|
||||
|
||||
async def get_jira_credentials(
|
||||
x_jira_email: str | None = Header(None),
|
||||
x_jira_token: str | None = Header(None),
|
||||
) -> JiraCredentials:
|
||||
"""
|
||||
Dependency that extracts Jira credentials from headers or falls back to config.
|
||||
|
||||
- Headers provided → per-request credentials (web demo)
|
||||
- No headers → use .env credentials (API/standalone)
|
||||
"""
|
||||
# Use headers if provided (check for non-empty strings)
|
||||
if x_jira_email and x_jira_token and x_jira_email.strip() and x_jira_token.strip():
|
||||
return JiraCredentials(email=x_jira_email.strip(), token=x_jira_token.strip())
|
||||
|
||||
# Fall back to config
|
||||
if settings.jira_email and settings.jira_api_token:
|
||||
return JiraCredentials(email=settings.jira_email, token=settings.jira_api_token)
|
||||
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail="Missing credentials: provide X-Jira-Email and X-Jira-Token headers, or configure in .env",
|
||||
)
|
||||
19
artery/veins/jira/core/client.py
Normal file
19
artery/veins/jira/core/client.py
Normal file
@@ -0,0 +1,19 @@
|
||||
"""
|
||||
Jira connection client.
|
||||
"""
|
||||
|
||||
from jira import JIRA
|
||||
|
||||
from .config import settings
|
||||
|
||||
|
||||
class JiraClientError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def connect_jira(email: str, token: str) -> JIRA:
|
||||
"""Create a Jira connection with the given credentials."""
|
||||
return JIRA(
|
||||
server=settings.jira_url,
|
||||
basic_auth=(email, token),
|
||||
)
|
||||
23
artery/veins/jira/core/config.py
Normal file
23
artery/veins/jira/core/config.py
Normal file
@@ -0,0 +1,23 @@
|
||||
"""
|
||||
Jira credentials loaded from .env file.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
ENV_FILE = Path(__file__).parent.parent / ".env"
|
||||
|
||||
|
||||
class JiraConfig(BaseSettings):
|
||||
jira_url: str
|
||||
jira_email: str | None = None # Optional: can be provided per-request via headers
|
||||
jira_api_token: str | None = None # Optional: can be provided per-request via headers
|
||||
api_port: int = 8001
|
||||
|
||||
model_config = {
|
||||
"env_file": ENV_FILE,
|
||||
"env_file_encoding": "utf-8",
|
||||
}
|
||||
|
||||
|
||||
settings = JiraConfig()
|
||||
86
artery/veins/jira/core/query.py
Normal file
86
artery/veins/jira/core/query.py
Normal file
@@ -0,0 +1,86 @@
|
||||
"""
|
||||
JQL query builder.
|
||||
"""
|
||||
|
||||
from typing import Optional, List
|
||||
|
||||
|
||||
class JQL:
|
||||
"""Fluent JQL builder."""
|
||||
|
||||
def __init__(self):
|
||||
self._parts: List[str] = []
|
||||
self._order: Optional[str] = None
|
||||
|
||||
def _q(self, val: str) -> str:
|
||||
return f'"{val}"' if " " in val else val
|
||||
|
||||
# Conditions
|
||||
def assigned_to_me(self) -> "JQL":
|
||||
self._parts.append("assignee = currentUser()")
|
||||
return self
|
||||
|
||||
def project(self, key: str) -> "JQL":
|
||||
self._parts.append(f"project = {self._q(key)}")
|
||||
return self
|
||||
|
||||
def sprint_open(self) -> "JQL":
|
||||
self._parts.append("sprint in openSprints()")
|
||||
return self
|
||||
|
||||
def in_backlog(self) -> "JQL":
|
||||
self._parts.append("sprint is EMPTY")
|
||||
return self
|
||||
|
||||
def not_done(self) -> "JQL":
|
||||
self._parts.append("statusCategory != Done")
|
||||
return self
|
||||
|
||||
def status(self, name: str) -> "JQL":
|
||||
self._parts.append(f"status = {self._q(name)}")
|
||||
return self
|
||||
|
||||
def label(self, name: str) -> "JQL":
|
||||
self._parts.append(f"labels = {self._q(name)}")
|
||||
return self
|
||||
|
||||
def text(self, search: str) -> "JQL":
|
||||
self._parts.append(f'text ~ "{search}"')
|
||||
return self
|
||||
|
||||
def issue_type(self, name: str) -> "JQL":
|
||||
self._parts.append(f"issuetype = {self._q(name)}")
|
||||
return self
|
||||
|
||||
def raw(self, jql: str) -> "JQL":
|
||||
self._parts.append(jql)
|
||||
return self
|
||||
|
||||
# Ordering
|
||||
def order_by(self, field: str, desc: bool = True) -> "JQL":
|
||||
self._order = f"ORDER BY {field} {'DESC' if desc else 'ASC'}"
|
||||
return self
|
||||
|
||||
def build(self) -> str:
|
||||
jql = " AND ".join(self._parts)
|
||||
if self._order:
|
||||
jql = f"{jql} {self._order}"
|
||||
return jql.strip()
|
||||
|
||||
|
||||
# Preset queries for main use cases
|
||||
class Queries:
|
||||
@staticmethod
|
||||
def my_tickets(project: Optional[str] = None) -> JQL:
|
||||
q = JQL().assigned_to_me().not_done().order_by("updated")
|
||||
if project:
|
||||
q.project(project)
|
||||
return q
|
||||
|
||||
@staticmethod
|
||||
def backlog(project: str) -> JQL:
|
||||
return JQL().project(project).in_backlog().not_done().order_by("priority")
|
||||
|
||||
@staticmethod
|
||||
def current_sprint(project: str) -> JQL:
|
||||
return JQL().project(project).sprint_open().order_by("priority")
|
||||
Reference in New Issue
Block a user