add fields.py for Jira field formatting abstraction
This commit is contained in:
		
							
								
								
									
										212
									
								
								cli/fields.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										212
									
								
								cli/fields.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,212 @@ | |||||||
|  | """ | ||||||
|  | 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 | ||||||
		Reference in New Issue
	
	Block a user