|
17 | 17 |
|
18 | 18 | import datetime |
19 | 19 | import re |
| 20 | +from enum import StrEnum |
| 21 | +from types import MappingProxyType |
20 | 22 | from typing import Any, Optional, TypedDict |
21 | 23 |
|
| 24 | +from chromestatus_openapi.models import ( |
| 25 | + MilestoneCurationResponse, |
| 26 | + ReleaseNoteFeature, |
| 27 | + SummaryProgressStep, |
| 28 | + SummarySuggestion, |
| 29 | +) |
22 | 30 | from google.cloud import ndb # type: ignore |
23 | 31 |
|
24 | 32 | import settings |
|
31 | 39 | ) |
32 | 40 | from internals.core_models import ( |
33 | 41 | FeatureEntry, |
| 42 | + FeatureSummaryProgressStep, |
| 43 | + FeatureSummarySuggestion, |
| 44 | + MilestoneCuration, |
34 | 45 | MilestoneSet, |
35 | 46 | ReviewResultProperty, |
36 | 47 | Stage, |
@@ -847,3 +858,238 @@ def gate_value_to_json_dict(gate: Gate) -> dict[str, Any]: |
847 | 858 | 'self_certify_eligible': self_certify_eligible, |
848 | 859 | 'survey_answers': survey_answers, |
849 | 860 | } |
| 861 | + |
| 862 | + |
| 863 | +# Default fallback category display name for unmapped FeatureEntry category IDs. |
| 864 | +DEFAULT_CATEGORY_NAME = core_enums.FEATURE_CATEGORIES[core_enums.MISC] |
| 865 | + |
| 866 | + |
| 867 | +# OpenAPI StrEnum definitions for Datastore-to-API status mappings. |
| 868 | +class OpenAPISummarySource(StrEnum): |
| 869 | + """OpenAPI string constants for release note summary origins.""" |
| 870 | + |
| 871 | + HUMAN = 'HUMAN' |
| 872 | + AI_APPLIED = 'AI_APPLIED' |
| 873 | + |
| 874 | + |
| 875 | +class OpenAPISuggestionStatus(StrEnum): |
| 876 | + """OpenAPI string constants for summary suggestion review statuses.""" |
| 877 | + |
| 878 | + PENDING = 'PENDING' |
| 879 | + APPLIED = 'APPLIED' |
| 880 | + REJECTED = 'REJECTED' |
| 881 | + DISCARDED = 'DISCARDED' |
| 882 | + |
| 883 | + |
| 884 | +class OpenAPIBaselineStatus(StrEnum): |
| 885 | + """OpenAPI string constants for WebDX Baseline compatibility statuses.""" |
| 886 | + |
| 887 | + NONE = 'none' |
| 888 | + LIMITED = 'limited' |
| 889 | + NEWLY = 'newly' |
| 890 | + WIDELY = 'widely' |
| 891 | + |
| 892 | + |
| 893 | +class OpenAPIProgressStepId(StrEnum): |
| 894 | + """OpenAPI string constants for AI summary progress step identifiers.""" |
| 895 | + |
| 896 | + UNKNOWN = 'UNKNOWN' |
| 897 | + SEARCH_MDN = 'SEARCH_MDN' |
| 898 | + VERIFY_DOC_LINK = 'VERIFY_DOC_LINK' |
| 899 | + READ_SPEC = 'READ_SPEC' |
| 900 | + READ_EXPLAINER = 'READ_EXPLAINER' |
| 901 | + |
| 902 | + |
| 903 | +class OpenAPIProgressStepStatus(StrEnum): |
| 904 | + """OpenAPI string constants for AI summary progress step execution statuses.""" |
| 905 | + |
| 906 | + IN_PROGRESS = 'IN_PROGRESS' |
| 907 | + SUCCESS = 'SUCCESS' |
| 908 | + FAILED = 'FAILED' |
| 909 | + RETRYING = 'RETRYING' |
| 910 | + |
| 911 | + |
| 912 | +class OpenAPICurationStatus(StrEnum): |
| 913 | + """OpenAPI string constants for milestone release note curation statuses.""" |
| 914 | + |
| 915 | + PENDING = 'PENDING' |
| 916 | + IN_REVIEW = 'IN_REVIEW' |
| 917 | + COMPLETED = 'COMPLETED' |
| 918 | + |
| 919 | + |
| 920 | +SUMMARY_SUGGESTION_STATUS_TO_API: MappingProxyType[ |
| 921 | + core_enums.SummarySuggestionStatus, OpenAPISuggestionStatus |
| 922 | +] = MappingProxyType( |
| 923 | + { |
| 924 | + core_enums.SummarySuggestionStatus.UNKNOWN: OpenAPISuggestionStatus.PENDING, |
| 925 | + core_enums.SummarySuggestionStatus.PROPOSED: OpenAPISuggestionStatus.PENDING, |
| 926 | + core_enums.SummarySuggestionStatus.PENDING: OpenAPISuggestionStatus.PENDING, |
| 927 | + core_enums.SummarySuggestionStatus.APPLIED: OpenAPISuggestionStatus.APPLIED, |
| 928 | + core_enums.SummarySuggestionStatus.REJECTED: OpenAPISuggestionStatus.REJECTED, |
| 929 | + core_enums.SummarySuggestionStatus.DISCARDED: OpenAPISuggestionStatus.DISCARDED, |
| 930 | + core_enums.SummarySuggestionStatus.SKIPPED: OpenAPISuggestionStatus.DISCARDED, |
| 931 | + } |
| 932 | +) |
| 933 | + |
| 934 | +BASELINE_STATUS_TO_API: MappingProxyType[ |
| 935 | + core_enums.BaselineStatus, OpenAPIBaselineStatus |
| 936 | +] = MappingProxyType( |
| 937 | + { |
| 938 | + core_enums.BaselineStatus.NONE: OpenAPIBaselineStatus.NONE, |
| 939 | + core_enums.BaselineStatus.LIMITED: OpenAPIBaselineStatus.LIMITED, |
| 940 | + core_enums.BaselineStatus.NEWLY: OpenAPIBaselineStatus.NEWLY, |
| 941 | + core_enums.BaselineStatus.WIDELY: OpenAPIBaselineStatus.WIDELY, |
| 942 | + } |
| 943 | +) |
| 944 | + |
| 945 | +PROGRESS_STEP_ID_TO_API: MappingProxyType[ |
| 946 | + core_enums.ProgressStepId, OpenAPIProgressStepId |
| 947 | +] = MappingProxyType( |
| 948 | + { |
| 949 | + core_enums.ProgressStepId.UNKNOWN: OpenAPIProgressStepId.UNKNOWN, |
| 950 | + core_enums.ProgressStepId.START: OpenAPIProgressStepId.UNKNOWN, |
| 951 | + core_enums.ProgressStepId.SEARCH_MDN: OpenAPIProgressStepId.SEARCH_MDN, |
| 952 | + core_enums.ProgressStepId.VERIFY_DOC_LINK: OpenAPIProgressStepId.VERIFY_DOC_LINK, |
| 953 | + core_enums.ProgressStepId.READ_SPEC: OpenAPIProgressStepId.READ_SPEC, |
| 954 | + core_enums.ProgressStepId.READ_EXPLAINER: OpenAPIProgressStepId.READ_EXPLAINER, |
| 955 | + core_enums.ProgressStepId.LLM_GENERATION: OpenAPIProgressStepId.UNKNOWN, |
| 956 | + core_enums.ProgressStepId.EVALUATION: OpenAPIProgressStepId.UNKNOWN, |
| 957 | + core_enums.ProgressStepId.SUCCESS: OpenAPIProgressStepId.UNKNOWN, |
| 958 | + } |
| 959 | +) |
| 960 | + |
| 961 | +PROGRESS_STEP_STATUS_TO_API: MappingProxyType[ |
| 962 | + core_enums.ProgressStepStatus, OpenAPIProgressStepStatus |
| 963 | +] = MappingProxyType( |
| 964 | + { |
| 965 | + core_enums.ProgressStepStatus.UNKNOWN: OpenAPIProgressStepStatus.IN_PROGRESS, |
| 966 | + core_enums.ProgressStepStatus.START: OpenAPIProgressStepStatus.IN_PROGRESS, |
| 967 | + core_enums.ProgressStepStatus.IN_PROGRESS: OpenAPIProgressStepStatus.IN_PROGRESS, |
| 968 | + core_enums.ProgressStepStatus.SUCCESS: OpenAPIProgressStepStatus.SUCCESS, |
| 969 | + core_enums.ProgressStepStatus.FAILED: OpenAPIProgressStepStatus.FAILED, |
| 970 | + core_enums.ProgressStepStatus.RETRYING: OpenAPIProgressStepStatus.RETRYING, |
| 971 | + } |
| 972 | +) |
| 973 | + |
| 974 | +MILESTONE_CURATION_STATUS_TO_API: MappingProxyType[ |
| 975 | + core_enums.MilestoneCurationStatus, OpenAPICurationStatus |
| 976 | +] = MappingProxyType( |
| 977 | + { |
| 978 | + core_enums.MilestoneCurationStatus.PENDING: OpenAPICurationStatus.PENDING, |
| 979 | + core_enums.MilestoneCurationStatus.IN_REVIEW: OpenAPICurationStatus.IN_REVIEW, |
| 980 | + core_enums.MilestoneCurationStatus.COMPLETED: OpenAPICurationStatus.COMPLETED, |
| 981 | + } |
| 982 | +) |
| 983 | + |
| 984 | + |
| 985 | +def feature_summary_suggestion_to_dict( |
| 986 | + suggestion: FeatureSummarySuggestion, |
| 987 | +) -> dict[str, Any]: |
| 988 | + """Converts a FeatureSummarySuggestion entity into a dict matching SummarySuggestion schema.""" |
| 989 | + status_str = SUMMARY_SUGGESTION_STATUS_TO_API.get( |
| 990 | + suggestion.status, OpenAPISuggestionStatus.PENDING |
| 991 | + ) |
| 992 | + baseline_str = BASELINE_STATUS_TO_API.get( |
| 993 | + suggestion.baseline_status, OpenAPIBaselineStatus.NONE |
| 994 | + ) |
| 995 | + |
| 996 | + model = SummarySuggestion( |
| 997 | + feature_id=suggestion.key.integer_id(), |
| 998 | + suggested_summary=suggestion.suggested_summary, |
| 999 | + original_summary=suggestion.original_summary, |
| 1000 | + status=status_str, |
| 1001 | + baseline_status=baseline_str, |
| 1002 | + reasoning=suggestion.generation_rationale, |
| 1003 | + suggested_doc_links=suggestion.suggested_doc_links or [], |
| 1004 | + version_token=suggestion.version_token, |
| 1005 | + created=_date_to_str(suggestion.created), |
| 1006 | + updated=_date_to_str(suggestion.updated), |
| 1007 | + ) |
| 1008 | + return model.to_dict() |
| 1009 | + |
| 1010 | + |
| 1011 | +def summary_progress_step_to_dict( |
| 1012 | + step: FeatureSummaryProgressStep, |
| 1013 | +) -> dict[str, Any]: |
| 1014 | + """Converts a FeatureSummaryProgressStep entity into a dict matching SummaryProgressStep schema.""" |
| 1015 | + step_str = PROGRESS_STEP_ID_TO_API.get( |
| 1016 | + step.step_id, OpenAPIProgressStepId.UNKNOWN |
| 1017 | + ) |
| 1018 | + status_str = PROGRESS_STEP_STATUS_TO_API.get( |
| 1019 | + step.status, OpenAPIProgressStepStatus.IN_PROGRESS |
| 1020 | + ) |
| 1021 | + |
| 1022 | + model = SummaryProgressStep( |
| 1023 | + step=step_str, |
| 1024 | + status=status_str, |
| 1025 | + message=step.message, |
| 1026 | + start_timestamp=_date_to_str(step.start_timestamp), |
| 1027 | + end_timestamp=_date_to_str(step.end_timestamp), |
| 1028 | + ) |
| 1029 | + return model.to_dict() |
| 1030 | + |
| 1031 | + |
| 1032 | +def milestone_curation_to_dict(curation: MilestoneCuration) -> dict[str, Any]: |
| 1033 | + """Converts a MilestoneCuration entity into a dict matching MilestoneCurationResponse schema.""" |
| 1034 | + status_str = MILESTONE_CURATION_STATUS_TO_API.get( |
| 1035 | + curation.status, OpenAPICurationStatus.PENDING |
| 1036 | + ) |
| 1037 | + |
| 1038 | + model = MilestoneCurationResponse( |
| 1039 | + milestone=curation.milestone, |
| 1040 | + status=status_str, |
| 1041 | + curator_emails=curation.curator_emails or [], |
| 1042 | + last_reviewed=_date_to_str(curation.updated), |
| 1043 | + ) |
| 1044 | + return model.to_dict() |
| 1045 | + |
| 1046 | + |
| 1047 | +def feature_entry_to_release_note_feature_dict( |
| 1048 | + fe: FeatureEntry, |
| 1049 | + applied_suggestion: FeatureSummarySuggestion | None = None, |
| 1050 | + baseline_status: core_enums.BaselineStatus | None = None, |
| 1051 | + has_applied_suggestion: bool | None = None, |
| 1052 | +) -> dict[str, Any]: |
| 1053 | + """Converts a FeatureEntry into a dict matching the ReleaseNoteFeature schema.""" |
| 1054 | + feature_id = fe.key.integer_id() |
| 1055 | + category_int = fe.category if fe.category is not None else 0 |
| 1056 | + category_name = core_enums.FEATURE_CATEGORIES.get( |
| 1057 | + category_int, DEFAULT_CATEGORY_NAME |
| 1058 | + ) |
| 1059 | + feature_type_int = fe.feature_type if fe.feature_type is not None else 0 |
| 1060 | + |
| 1061 | + if ( |
| 1062 | + applied_suggestion is not None |
| 1063 | + and applied_suggestion.status |
| 1064 | + == core_enums.SummarySuggestionStatus.APPLIED |
| 1065 | + ): |
| 1066 | + summary_text = applied_suggestion.suggested_summary or fe.summary or '' |
| 1067 | + summary_source = OpenAPISummarySource.AI_APPLIED |
| 1068 | + effective_baseline = ( |
| 1069 | + applied_suggestion.baseline_status or baseline_status |
| 1070 | + ) |
| 1071 | + elif has_applied_suggestion: |
| 1072 | + summary_text = fe.summary or '' |
| 1073 | + summary_source = OpenAPISummarySource.AI_APPLIED |
| 1074 | + effective_baseline = baseline_status |
| 1075 | + else: |
| 1076 | + summary_text = fe.summary or '' |
| 1077 | + summary_source = OpenAPISummarySource.HUMAN |
| 1078 | + effective_baseline = baseline_status |
| 1079 | + |
| 1080 | + baseline_val = BASELINE_STATUS_TO_API.get( |
| 1081 | + effective_baseline or core_enums.BaselineStatus.NONE, |
| 1082 | + OpenAPIBaselineStatus.NONE, |
| 1083 | + ) |
| 1084 | + |
| 1085 | + model = ReleaseNoteFeature( |
| 1086 | + id=feature_id, |
| 1087 | + name=fe.name or '', |
| 1088 | + summary=summary_text, |
| 1089 | + category=category_int, |
| 1090 | + category_name=category_name, |
| 1091 | + feature_type=feature_type_int, |
| 1092 | + baseline_status=baseline_val, |
| 1093 | + summary_source=summary_source, |
| 1094 | + ) |
| 1095 | + return model.to_dict() |
0 commit comments