Coverage for src / qdrant_loader / connectors / jira / config.py: 95%
63 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-10 09:40 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-10 09:40 +0000
1"""Configuration for Jira connector."""
3import os
4from enum import StrEnum
5from typing import Self
7from pydantic import ConfigDict, Field, HttpUrl, field_validator, model_validator
9from qdrant_loader.config.source_config import SourceConfig
12class JiraDeploymentType(StrEnum):
13 """Jira deployment types."""
15 CLOUD = "cloud"
16 DATACENTER = "datacenter"
19class JiraProjectConfig(SourceConfig):
20 """Configuration for a Jira project."""
22 # Authentication
23 token: str | None = Field(
24 default=None, description="Jira API token or Personal Access Token"
25 )
26 email: str | None = Field(
27 default=None, description="Email associated with the API token (Cloud only)"
28 )
29 base_url: HttpUrl = Field(
30 ...,
31 description="Base URL of the Jira instance (e.g., 'https://your-domain.atlassian.net')",
32 )
34 # Project configuration
35 project_key: str = Field(
36 ..., description="Project key to process (e.g., 'PROJ')", min_length=1
37 )
39 # Deployment type
40 deployment_type: JiraDeploymentType = Field(
41 default=JiraDeploymentType.CLOUD,
42 description="Jira deployment type (cloud, datacenter, or server)",
43 )
45 # Rate limiting
46 requests_per_minute: int = Field(
47 default=60, description="Maximum number of requests per minute", ge=1, le=1000
48 )
50 # Pagination
51 page_size: int = Field(
52 default=100,
53 description="Number of items per page for paginated requests",
54 ge=1,
55 le=100,
56 )
58 # Attachment handling
59 download_attachments: bool = Field(
60 default=False, description="Whether to download and process issue attachments"
61 )
63 # Additional configuration
64 issue_types: list[str] = Field(
65 default=[],
66 description="Optional list of issue types to process (e.g., ['Bug', 'Story']). If empty, all types are processed.",
67 )
68 include_statuses: list[str] = Field(
69 default=[],
70 description="Optional list of statuses to include (e.g., ['Open', 'In Progress']). If empty, all statuses are included.",
71 )
73 model_config = ConfigDict(validate_default=True, arbitrary_types_allowed=True)
75 @field_validator("deployment_type", mode="before")
76 @classmethod
77 def auto_detect_deployment_type(
78 cls, v: str | JiraDeploymentType
79 ) -> JiraDeploymentType:
80 """Auto-detect deployment type if not specified."""
81 if isinstance(v, str):
82 return JiraDeploymentType(v.lower())
83 return v
85 @field_validator("token", mode="after")
86 @classmethod
87 def load_token_from_env(cls, v: str | None) -> str | None:
88 """Load token from environment variable if not provided."""
89 return v or os.getenv("JIRA_TOKEN")
91 @field_validator("email", mode="after")
92 @classmethod
93 def load_email_from_env(cls, v: str | None) -> str | None:
94 """Load email from environment variable if not provided."""
95 return v or os.getenv("JIRA_EMAIL")
97 @model_validator(mode="after")
98 def validate_no_placeholders(self) -> Self:
99 """Fail immediately if any required field still contains an un-substituted ${VAR} placeholder."""
100 import re
102 _placeholder = re.compile(r"\$\{[^}]+\}")
104 fields_to_check: dict[str, str | None] = {
105 "project_key": self.project_key,
106 "base_url": str(self.base_url) if self.base_url else None,
107 "token": self.token,
108 "email": self.email,
109 }
111 bad: list[str] = []
112 for field_name, value in fields_to_check.items():
113 if value and _placeholder.search(value):
114 # Extract the variable name for a helpful hint
115 var = _placeholder.search(value).group(0) # type: ignore[union-attr]
116 bad.append(f" - {field_name}: {var} (env var not set)")
118 if bad:
119 raise ValueError(
120 "Jira source config contains un-substituted environment variables.\n"
121 "Set the following variables in your .env file or shell before running:\n"
122 + "\n".join(bad)
123 )
125 return self
127 @model_validator(mode="after")
128 def validate_auth_config(self) -> Self:
129 """Validate authentication configuration based on deployment type."""
130 if self.deployment_type == JiraDeploymentType.CLOUD:
131 # Cloud requires email and token
132 if not self.email:
133 raise ValueError("Email is required for Jira Cloud deployment")
134 if not self.token:
135 raise ValueError("API token is required for Jira Cloud deployment")
136 else:
137 # Data Center/Server requires Personal Access Token
138 if not self.token:
139 raise ValueError(
140 "Personal Access Token is required for Jira Data Center/Server deployment"
141 )
143 return self
145 @field_validator("issue_types", "include_statuses")
146 @classmethod
147 def validate_list_items(cls, v: list[str]) -> list[str]:
148 """Validate that list items are not empty strings."""
149 if any(not item.strip() for item in v):
150 raise ValueError("List items cannot be empty strings")
151 return [item.strip() for item in v]