""" Jira Field Utilities for CLI-based Issue Updates. This module provides a clean abstraction for working with Jira issue fields in a CLI or automation context. It hides the complexity of Jira's API field IDs (e.g. "customfield_10302") and automatically formats values according to the field's type, allowing developers to reference fields by friendly names instead of raw Jira IDs. Key Components: - FieldType: Enum describing how each Jira field type (e.g. single select, multi select, user picker) must be structured for API updates. - JiraFields: Developer-friendly enum for referencing Jira fields by logical names (e.g. RELEASE_TRAIN) instead of raw custom field IDs. - JiraFieldInfo: Dataclass tying together a Jira field's API ID and its FieldType, along with an appropriate formatter. - FIELD_REGISTRY: The single source of truth mapping JiraFields to their JiraFieldInfo, used to build correctly formatted update payloads. - UpdateFields: A builder class for collecting field changes and generating a payload dictionary ready to pass into jira.issue.update(fields=...). Usage Example: >>> update_fields = UpdateFields() >>> update_fields.add_field(JiraFields.RELEASE_TRAIN, "Beta Train") >>> update_fields.add_field(JiraFields.REPORTER, "jdoe") >>> jira.issue("AETHER-1").update(fields=update_fields.as_dict()) Notes: - Currently defaults to Jira Server/DC formatting for user fields ({"name": ...}). Jira Cloud users can easily switch to {"accountId": ...} by swapping the formatter for FieldType.USER. - FIELD_REGISTRY is the single source of truth for all Jira field mappings. To add new fields (e.g. cascading select or date fields), simply register them here with their appropriate type and formatter. This design centralizes all field formatting rules and mappings, making Jira updates cleaner, safer, and easier to maintain. """ from dataclasses import dataclass from enum import Enum from typing import Any, Callable class FieldType(Enum): """ Enumeration of supported Jira field types used by UpdateFields. Each value represents how data for that field type should be structured when sent to the Jira API. The value is paired with a formatter function in FIELD_FORMATTERS to handle the conversion. Members: TEXT: A simple text or raw value (string, number, etc.) sent as-is. SINGLE_SELECT: A single-choice select list, formatted as {"value": "..."}. MULTI_SELECT: A multi-choice select list, formatted as [{"value": "..."}, ...]. USER: A user picker field, typically formatted as {"name": "..."} on Server/DC, or {"accountId": "..."} on Jira Cloud. GROUP: A group picker field, formatted as {"name": "..."}. DATE: A date field, expected in ISO-8601 string format (e.g., "2025-07-26"). LABELS: A label field, sent as a list of strings. """ TEXT = "text" SINGLE_SELECT = "single_select" MULTI_SELECT = "multi_select" USER = "user" GROUP = "group" DATE = "date" LABELS = "labels" def single_select_formatter(value: Any): return {"value": value} def multi_select_formatter(values: Any): return [{"value": value} for value in (values if isinstance(values, list) else [values])] def user_formatter(value: Any): return {"name": value} def labels_formatter(value: Any): return value if isinstance(value, list) else [value] FIELD_FORMATTERS = { FieldType.TEXT: lambda v: v, FieldType.SINGLE_SELECT: single_select_formatter, FieldType.MULTI_SELECT: multi_select_formatter, FieldType.USER: user_formatter, FieldType.GROUP: user_formatter, # same shape as USER on Server/DC FieldType.DATE: lambda v: v, # could later enforce ISO 8601 FieldType.LABELS: labels_formatter, } class JiraFields(Enum): """ Enumeration of high-level Jira field names used in the CLI. This enum provides developer-friendly identifiers for Jira fields, hiding Jira's internal custom field IDs (e.g. "customfield_10302"). Members: REPORTER: The user who created or is assigned responsibility for the issue. RELEASE_TRAIN: A single-select custom field for specifying the release train. DEPLOYMENT_REQUIREMENTS: A multi-select custom field for deployment prerequisites. Notes: JiraFields values are mapped to Jira API field IDs and types through the FIELD_REGISTRY, which is the single source of truth for how each field is formatted and sent to Jira. """ REPORTER = "reporter" RELEASE_TRAIN = "release_train" DEPLOYMENT_REQUIREMENTS = "deployment_requirements" def __str__(self): return self.value def __eq__(self, other): if isinstance(other, str): return self.value == other return super().__eq__(other) def __hash__(self): return hash((self.name, self.value)) @dataclass class JiraFieldInfo: """ Metadata container for a Jira field. Each JiraFieldInfo instance holds the information needed to correctly serialize a Jira field for API updates. Attributes: field_id (str): The Jira API identifier for the field (e.g. "customfield_10302" or "reporter"). field_type (FieldType): The type of field, which determines how its value should be formatted. Properties: formatter (Callable[[Any], Any]): Returns the correct formatter function for the field_type, so values are automatically wrapped or structured as required by the Jira API. Usage: >>> FIELD_REGISTRY[JiraFields.RELEASE_TRAIN].formatter("Beta Train") {"value": "Beta Train"} """ field_id: str field_type: FieldType @property def formatter(self) -> Callable[[Any], Any]: return FIELD_FORMATTERS[self.field_type] FIELD_REGISTRY = { JiraFields.RELEASE_TRAIN: JiraFieldInfo( field_id="customfield_10302", field_type=FieldType.SINGLE_SELECT, ), JiraFields.DEPLOYMENT_REQUIREMENTS: JiraFieldInfo( field_id="customfield_10300", field_type=FieldType.MULTI_SELECT, ), JiraFields.REPORTER: JiraFieldInfo( field_id="reporter", field_type=FieldType.USER, ), } class UpdateFields: """ Builder class for constructing Jira issue field update payloads. This class uses FIELD_REGISTRY to look up how each JiraFields enum member should be formatted and which Jira API field ID it maps to. The resulting dictionary can be passed directly to Jira's issue.update() method. Attributes: fields (dict[str, Any]): A dictionary mapping Jira API field IDs to their correctly formatted values. Methods: add_field(name: JiraFields, value: Any): Adds a field to the update payload. Automatically applies the correct formatter based on the field's FieldType. as_dict() -> dict: Returns the completed payload as a dictionary suitable for passing to jira.issue.update(fields=...). Example: >>> update_fields = UpdateFields() >>> update_fields.add_field(JiraFields.RELEASE_TRAIN, "Beta Train") >>> update_fields.add_field(JiraFields.REPORTER, "jdoe") >>> jira.issue("AETHER-1").update(fields=update_fields.as_dict()) """ def __init__(self): self.fields = {} def add_field(self, name: JiraFields, value: Any): if not isinstance(name, JiraFields): raise ValueError(f"Expected JiraFields enum, got {type(name)}") info: JiraFieldInfo = FIELD_REGISTRY[name] self.fields[info.field_id] = info.formatter(value) def as_dict(self): return self.fields