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