migrated all pawprint work

This commit is contained in:
buenosairesam
2025-12-31 08:34:18 -03:00
parent fc63e9010c
commit 680969ca42
63 changed files with 4687 additions and 5 deletions

View File

View 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",
)

View 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),
)

View 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()

View 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")