-
Notifications
You must be signed in to change notification settings - Fork 72
/
activities.py
182 lines (148 loc) · 7.7 KB
/
activities.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
from ruamel.yaml import YAML
from ruamel.yaml.comments import CommentedMap
from ruamel.yaml.scalarstring import LiteralScalarString
import argparse
import requests
import sys
from typing import Any, Dict
class YAMLValidator:
def __init__(self, file_path: str, fix: bool = False):
self.file_path = file_path
self.fix = fix
self.errors = []
self.modified_data = None
self.yaml = YAML()
self.yaml.preserve_quotes = True
def load_yaml(self) -> CommentedMap:
try:
with open(self.file_path, 'r') as file:
data = self.yaml.load(file)
return data
except Exception as e:
raise ValueError(f"Failed to load YAML: {e}") from e
def log_error(self, message: str, key: str = ""):
self.errors.append(f"Error in key '{key}': {message}")
def validate_top_level_keys(self, data: CommentedMap):
keys = list(data.keys())
if keys != sorted(keys):
if self.fix:
self.modified_data = CommentedMap(sorted(data.items()))
else:
self.log_error("Top-level keys must be in alphabetical order.")
def validate_literal_block(self, data: CommentedMap, key: str, field: str):
value = data.get(key, {}).get(field)
if value is not None:
if not isinstance(value, LiteralScalarString):
if self.fix:
data[key][field] = LiteralScalarString(value)
else:
self.log_error(f"'{field}' must use literal block syntax (|).", key)
elif not value.endswith('\n'):
if self.fix:
data[key][field] = LiteralScalarString(f"{value}\n")
else:
self.log_error(f"'{field}' must end with a newline to use '|' syntax.", key)
def validate_item(self, data: CommentedMap, key: str):
item = data[key]
if not isinstance(item, dict):
self.log_error(f"Item '{key}' must be a dictionary.", key)
return
# Validate required fields
if 'issue' not in item:
self.log_error("Missing required 'issue' key.", key)
elif not isinstance(item['issue'], int):
self.log_error(f"'issue' must be an integer, got {item['issue']}.", key)
elif item['issue'] >= 1110:
# Legacy item key restriction for issue numbers >= 1110
for legacy_key in ['position', 'venues']:
if legacy_key in item:
if self.fix:
del item[legacy_key]
else:
self.log_error(f"Legacy key '{legacy_key}' is not allowed for issue numbers >= 1110.", key)
# Validate literal block fields
for field in ['description', 'rationale']:
self.validate_literal_block(data, key, field)
# Optional fields validation
if 'id' in item and (not isinstance(item['id'], str) or ' ' in item['id']):
self.log_error(f"'id' must be a string without whitespace, got {item['id']}.", key)
if 'bug' in item and item['bug'] not in [None, '']:
if not isinstance(item['bug'], str) or not item['bug'].startswith("https://bugzilla.mozilla.org/show_bug.cgi?id="):
self.log_error(f"'bug' must be a URL starting with 'https://bugzilla.mozilla.org/show_bug.cgi?id=', got {item['bug']}.", key)
if 'caniuse' in item and item['caniuse'] not in [None, '']:
if not isinstance(item['caniuse'], str) or not item['caniuse'].startswith("https://caniuse.com/"):
self.log_error(f"'caniuse' must be a URL starting with 'https://caniuse.com/', got {item['caniuse']}.", key)
if 'mdn' in item and item['mdn'] not in [None, '']:
if not isinstance(item['mdn'], str) or not item['mdn'].startswith("https://developer.mozilla.org/en-US/"):
self.log_error(f"'mdn' must be a URL starting with 'https://developer.mozilla.org/en-US/', got {item['mdn']}.", key)
if 'position' in item and item['position'] not in {"positive", "neutral", "negative", "defer", "under consideration"}:
self.log_error(f"'position' must be one of the allowed values, got {item['position']}.", key)
if 'url' in item and not item['url'].startswith("https://"):
self.log_error(f"'url' must start with 'https://', got {item['url']}.", key)
if 'venues' in item:
allowed_venues = {"WHATWG", "W3C", "W3C CG", "IETF", "Ecma", "Unicode", "Proposal", "Other"}
if not isinstance(item['venues'], list) or not set(item['venues']).issubset(allowed_venues):
self.log_error(f"'venues' must be a list with allowed values, got {item['venues']}.", key)
def validate_data(self, data: CommentedMap):
for key in data:
self.validate_item(data, key)
self.validate_top_level_keys(data)
if self.fix and self.modified_data is None:
self.modified_data = data
def save_fixes(self):
if self.modified_data:
with open(self.file_path, 'w') as file:
self.yaml.dump(self.modified_data, file)
print(f"Fixes applied and saved to {self.file_path}")
def run(self):
data = self.load_yaml()
self.validate_data(data)
if self.errors:
print("YAML validation failed with the following errors:")
for error in self.errors:
print(error)
sys.exit(1)
elif self.fix:
self.save_fixes()
def add_issue(self, issue_num: int, description: str = None, rationale: str = None):
url = f"https://api.github.com/repos/mozilla/standards-positions/issues/{issue_num}"
response = requests.get(url)
if response.status_code != 200:
print(f"Failed to fetch issue {issue_num}: {response.status_code}")
sys.exit(1)
issue_data = response.json()
title = issue_data.get("title")
if not title:
print("No title found in the GitHub issue data.")
sys.exit(1)
data = self.load_yaml()
if title not in data:
data[title] = {"issue": issue_num}
if description:
data[title]["description"] = LiteralScalarString(f"{description}\n")
if rationale:
data[title]["rationale"] = LiteralScalarString(f"{rationale}\n")
# Sort keys alphabetically
self.modified_data = CommentedMap(sorted(data.items()))
with open(self.file_path, 'w') as file:
self.yaml.dump(self.modified_data, file)
print(f"Issue {issue_num} added or updated successfully.")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Manage activities.yml.")
parser.add_argument("command", choices=["validate", "add"], help="Specify 'validate' to validate or 'add' to add or update an item.")
parser.add_argument("issue_num", nargs="?", type=int, help="Issue number for the 'add' command.")
parser.add_argument("--fix", action="store_true", help="Automatically fix issues.")
parser.add_argument("--description", type=str, help="Set the description for the issue.")
parser.add_argument("--rationale", type=str, help="Set the rationale for the issue.")
args = parser.parse_args()
validator = YAMLValidator("activities.yml", fix=args.fix)
if args.command == "validate":
try:
validator.run()
except ValueError as e:
print(f"Validation failed: {e}")
elif args.command == "add":
if args.issue_num is None:
print("Please provide an issue number for 'add'.")
sys.exit(1)
validator.add_issue(args.issue_num, description=args.description, rationale=args.rationale)