Update fields
This commit is contained in:
@ -36,16 +36,19 @@ Notes:
|
|||||||
This design centralizes all field formatting rules and mappings, making Jira
|
This design centralizes all field formatting rules and mappings, making Jira
|
||||||
updates cleaner, safer, and easier to maintain.
|
updates cleaner, safer, and easier to maintain.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Any, Callable
|
from typing import Any, Callable
|
||||||
|
|
||||||
|
from dateutil import parser
|
||||||
from jira import Issue
|
from jira import Issue
|
||||||
from jira.resources import CustomFieldOption, User
|
from jira.resources import CustomFieldOption, User, Resource
|
||||||
|
|
||||||
|
|
||||||
class DeploymentRequirements(Enum):
|
class DeploymentRequirements(Enum):
|
||||||
"""Checklist of deployment requirements for the Jira 'Deployment Requirements' multi-select field."""
|
"""Checklist of deployment requirements for the Jira 'Deployment Requirements' multi-select field."""
|
||||||
|
|
||||||
CODE_REVIEW_COMPLETED = "Code Review Completed"
|
CODE_REVIEW_COMPLETED = "Code Review Completed"
|
||||||
UNIT_TESTS_PASSED = "Unit Tests Passed"
|
UNIT_TESTS_PASSED = "Unit Tests Passed"
|
||||||
QA_SIGN_OFF = "QA Sign-off"
|
QA_SIGN_OFF = "QA Sign-off"
|
||||||
@ -56,6 +59,7 @@ class DeploymentRequirements(Enum):
|
|||||||
|
|
||||||
class ReleaseTrain(Enum):
|
class ReleaseTrain(Enum):
|
||||||
"""Valid release train options for the Jira 'Release Train' single-select field."""
|
"""Valid release train options for the Jira 'Release Train' single-select field."""
|
||||||
|
|
||||||
ALPHA_TRAIN = "Alpha Train"
|
ALPHA_TRAIN = "Alpha Train"
|
||||||
BETA_TRAIN = "Beta Train"
|
BETA_TRAIN = "Beta Train"
|
||||||
GAMMA_TRAIN = "Gamma Train"
|
GAMMA_TRAIN = "Gamma Train"
|
||||||
@ -83,6 +87,7 @@ class FieldType(Enum):
|
|||||||
DATE: A date field, expected in ISO-8601 string format (e.g., "2025-07-26").
|
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.
|
LABELS: A label field, sent as a list of strings.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
TEXT = "text"
|
TEXT = "text"
|
||||||
SINGLE_SELECT = "single_select"
|
SINGLE_SELECT = "single_select"
|
||||||
MULTI_SELECT = "multi_select"
|
MULTI_SELECT = "multi_select"
|
||||||
@ -90,25 +95,42 @@ class FieldType(Enum):
|
|||||||
GROUP = "group"
|
GROUP = "group"
|
||||||
DATE = "date"
|
DATE = "date"
|
||||||
LABELS = "labels"
|
LABELS = "labels"
|
||||||
|
ENTITY = "entity"
|
||||||
|
|
||||||
def single_select_formatter(value: Any):
|
|
||||||
|
def single_select_formatter(value: Any) -> dict[str, str]:
|
||||||
if isinstance(value, Enum):
|
if isinstance(value, Enum):
|
||||||
return {"value": value.value}
|
return {"value": value.value}
|
||||||
return {"value": value}
|
return {"value": value}
|
||||||
|
|
||||||
def multi_select_formatter(values: Any):
|
|
||||||
|
def multi_select_formatter(values: Any) -> list[dict[str, str]]:
|
||||||
if all(isinstance(v, Enum) for v in values):
|
if all(isinstance(v, Enum) for v in values):
|
||||||
return [{"value": v.value} for v in values]
|
return [{"value": v.value} for v in values]
|
||||||
return [{"value": value} for value in (values if isinstance(values, list) else [values])]
|
return [
|
||||||
|
{"value": value} for value in (values if isinstance(values, list) else [values])
|
||||||
|
]
|
||||||
|
|
||||||
def user_formatter(value: Any):
|
|
||||||
|
def user_formatter(value: Any) -> dict[str, str]:
|
||||||
return {"name": value}
|
return {"name": value}
|
||||||
|
|
||||||
def labels_formatter(value: Any):
|
|
||||||
|
def labels_formatter(value: Any) -> list[str]:
|
||||||
return value if isinstance(value, list) else [value]
|
return value if isinstance(value, list) else [value]
|
||||||
|
|
||||||
|
|
||||||
FIELD_FORMATTERS = {
|
def entity_formatter(value: Any) -> dict[str, str]:
|
||||||
|
"""Format entity fields like issuetype/status when updating."""
|
||||||
|
# allow passing either an Enum, str, or id dict
|
||||||
|
if isinstance(value, Enum):
|
||||||
|
return {"name": value.value}
|
||||||
|
if isinstance(value, dict) and ("name" in value or "id" in value):
|
||||||
|
return value
|
||||||
|
return {"name": value}
|
||||||
|
|
||||||
|
|
||||||
|
FIELD_FORMATTERS: dict[FieldType, Callable[..., Any]] = {
|
||||||
FieldType.TEXT: lambda v: v,
|
FieldType.TEXT: lambda v: v,
|
||||||
FieldType.SINGLE_SELECT: single_select_formatter,
|
FieldType.SINGLE_SELECT: single_select_formatter,
|
||||||
FieldType.MULTI_SELECT: multi_select_formatter,
|
FieldType.MULTI_SELECT: multi_select_formatter,
|
||||||
@ -116,6 +138,7 @@ FIELD_FORMATTERS = {
|
|||||||
FieldType.GROUP: user_formatter, # same shape as USER on Server/DC
|
FieldType.GROUP: user_formatter, # same shape as USER on Server/DC
|
||||||
FieldType.DATE: lambda v: v, # could later enforce ISO 8601
|
FieldType.DATE: lambda v: v, # could later enforce ISO 8601
|
||||||
FieldType.LABELS: labels_formatter,
|
FieldType.LABELS: labels_formatter,
|
||||||
|
FieldType.ENTITY: entity_formatter,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -139,12 +162,29 @@ def get_user_formatter(user: User) -> dict:
|
|||||||
return user.name
|
return user.name
|
||||||
|
|
||||||
|
|
||||||
GET_FIELD_FORMATTERS = {
|
def get_date_formatter(date_str: str) -> str:
|
||||||
|
try:
|
||||||
|
return parser.parse(date_str)
|
||||||
|
except ValueError as e:
|
||||||
|
raise ValueError(f"Invalid date format: {date_str}") from e
|
||||||
|
|
||||||
|
|
||||||
|
def get_entity_formatter(value: Resource) -> str:
|
||||||
|
"""Extract the 'name' from an entity field (issuetype, status)."""
|
||||||
|
if not isinstance(value, Resource):
|
||||||
|
raise ValueError(f"Expected Resource for ENTITY field, got {type(value)}")
|
||||||
|
return value.name
|
||||||
|
|
||||||
|
|
||||||
|
GET_FIELD_FORMATTERS: dict[FieldType, Callable[..., Any]] = {
|
||||||
FieldType.TEXT: lambda v: v,
|
FieldType.TEXT: lambda v: v,
|
||||||
FieldType.SINGLE_SELECT: get_single_select_formatter,
|
FieldType.SINGLE_SELECT: get_single_select_formatter,
|
||||||
FieldType.MULTI_SELECT: get_multi_select_formatter,
|
FieldType.MULTI_SELECT: get_multi_select_formatter,
|
||||||
FieldType.USER: get_user_formatter,
|
FieldType.USER: get_user_formatter,
|
||||||
FieldType.LABELS: lambda v: v,
|
FieldType.LABELS: lambda v: v,
|
||||||
|
FieldType.DATE: get_date_formatter,
|
||||||
|
FieldType.GROUP: get_user_formatter,
|
||||||
|
FieldType.ENTITY: get_entity_formatter,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -165,9 +205,16 @@ class JiraFields(Enum):
|
|||||||
through the FIELD_REGISTRY, which is the single source of truth
|
through the FIELD_REGISTRY, which is the single source of truth
|
||||||
for how each field is formatted and sent to Jira.
|
for how each field is formatted and sent to Jira.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
REPORTER = "reporter"
|
REPORTER = "reporter"
|
||||||
RELEASE_TRAIN = "release_train"
|
RELEASE_TRAIN = "release_train"
|
||||||
DEPLOYMENT_REQUIREMENTS = "deployment_requirements"
|
DEPLOYMENT_REQUIREMENTS = "deployment_requirements"
|
||||||
|
CREATED = "created"
|
||||||
|
UPDATED = "updated"
|
||||||
|
STATUS = "status"
|
||||||
|
ISSUETYPE = "issuetype"
|
||||||
|
PROJECT = "project"
|
||||||
|
PRIORITY = "priority"
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.value
|
return self.value
|
||||||
@ -196,7 +243,7 @@ class JiraFieldInfo:
|
|||||||
how its value should be formatted.
|
how its value should be formatted.
|
||||||
|
|
||||||
Properties:
|
Properties:
|
||||||
formatter (Callable[[Any], Any]): Returns the correct formatter
|
formatter (Callable[..., Any]): Returns the correct formatter
|
||||||
function for the field_type, so values are automatically
|
function for the field_type, so values are automatically
|
||||||
wrapped or structured as required by the Jira API.
|
wrapped or structured as required by the Jira API.
|
||||||
|
|
||||||
@ -204,15 +251,16 @@ class JiraFieldInfo:
|
|||||||
>>> FIELD_REGISTRY[JiraFields.RELEASE_TRAIN].formatter("Beta Train")
|
>>> FIELD_REGISTRY[JiraFields.RELEASE_TRAIN].formatter("Beta Train")
|
||||||
{"value": "Beta Train"}
|
{"value": "Beta Train"}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
field_id: str
|
field_id: str
|
||||||
field_type: FieldType
|
field_type: FieldType
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def formatter(self) -> Callable[[Any], Any]:
|
def formatter(self) -> Callable[..., Any]:
|
||||||
return FIELD_FORMATTERS[self.field_type]
|
return FIELD_FORMATTERS[self.field_type]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def get_field_formatter(self) -> Callable[[Any], Any]:
|
def get_field_formatter(self) -> Callable[..., Any]:
|
||||||
return GET_FIELD_FORMATTERS[self.field_type]
|
return GET_FIELD_FORMATTERS[self.field_type]
|
||||||
|
|
||||||
|
|
||||||
@ -229,6 +277,30 @@ FIELD_REGISTRY = {
|
|||||||
field_id="reporter",
|
field_id="reporter",
|
||||||
field_type=FieldType.USER,
|
field_type=FieldType.USER,
|
||||||
),
|
),
|
||||||
|
JiraFields.CREATED: JiraFieldInfo(
|
||||||
|
field_id="created",
|
||||||
|
field_type=FieldType.DATE,
|
||||||
|
),
|
||||||
|
JiraFields.UPDATED: JiraFieldInfo(
|
||||||
|
field_id="updated",
|
||||||
|
field_type=FieldType.DATE,
|
||||||
|
),
|
||||||
|
JiraFields.STATUS: JiraFieldInfo(
|
||||||
|
field_id="status",
|
||||||
|
field_type=FieldType.ENTITY,
|
||||||
|
),
|
||||||
|
JiraFields.ISSUETYPE: JiraFieldInfo(
|
||||||
|
field_id="issuetype",
|
||||||
|
field_type=FieldType.ENTITY,
|
||||||
|
),
|
||||||
|
JiraFields.PROJECT: JiraFieldInfo(
|
||||||
|
field_id="project",
|
||||||
|
field_type=FieldType.ENTITY,
|
||||||
|
),
|
||||||
|
JiraFields.PRIORITY: JiraFieldInfo(
|
||||||
|
field_id="priority",
|
||||||
|
field_type=FieldType.ENTITY,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -256,7 +328,9 @@ def get_field(issue: Issue, name: JiraFields) -> JiraFieldInfo:
|
|||||||
try:
|
try:
|
||||||
data = getattr(issue.fields, info.field_id)
|
data = getattr(issue.fields, info.field_id)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
raise ValueError(f"Issue does not have field {name} ({FIELD_REGISTRY[name].field_id})")
|
raise ValueError(
|
||||||
|
f"Issue does not have field {name} ({FIELD_REGISTRY[name].field_id})"
|
||||||
|
)
|
||||||
return info.get_field_formatter(data)
|
return info.get_field_formatter(data)
|
||||||
|
|
||||||
|
|
||||||
@ -288,6 +362,7 @@ class UpdateFields:
|
|||||||
>>> update_fields.add_field(JiraFields.REPORTER, "jdoe")
|
>>> update_fields.add_field(JiraFields.REPORTER, "jdoe")
|
||||||
>>> jira.issue("AETHER-1").update(fields=update_fields.as_dict())
|
>>> jira.issue("AETHER-1").update(fields=update_fields.as_dict())
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.fields = {}
|
self.fields = {}
|
||||||
|
|
||||||
|
@ -17,6 +17,21 @@ tick = jira.issue("AETHER-1")
|
|||||||
|
|
||||||
deployment_requirements = get_field(tick, jf.DEPLOYMENT_REQUIREMENTS)
|
deployment_requirements = get_field(tick, jf.DEPLOYMENT_REQUIREMENTS)
|
||||||
reporter = get_field(tick, jf.REPORTER)
|
reporter = get_field(tick, jf.REPORTER)
|
||||||
|
created_date = get_field(tick, jf.CREATED)
|
||||||
|
updated_date = get_field(tick, jf.UPDATED)
|
||||||
|
status = get_field(tick, jf.STATUS)
|
||||||
|
issuetype = get_field(tick, jf.ISSUETYPE)
|
||||||
|
project = get_field(tick, jf.PROJECT)
|
||||||
|
priority = get_field(tick, jf.PRIORITY)
|
||||||
|
print(f"{created_date=}", type(created_date))
|
||||||
|
print(f"{updated_date=}", type(updated_date))
|
||||||
|
print(f"{reporter=}", type(reporter))
|
||||||
|
print(f"{deployment_requirements=}", type(deployment_requirements))
|
||||||
|
print(f"{status=}", type(status))
|
||||||
|
print(f"{issuetype=}", type(issuetype))
|
||||||
|
print(f"{project=}", type(project))
|
||||||
|
print(f"{priority=}", type(priority))
|
||||||
|
|
||||||
|
|
||||||
update_fields = UpdateFields()
|
update_fields = UpdateFields()
|
||||||
update_fields.add_field(jf.RELEASE_TRAIN, rt.GAMMA_TRAIN)
|
update_fields.add_field(jf.RELEASE_TRAIN, rt.GAMMA_TRAIN)
|
||||||
|
Reference in New Issue
Block a user