joshhayles commited on
Commit
0c221a9
·
verified ·
1 Parent(s): c7970ef

Upload 3 files

Browse files
Files changed (3) hide show
  1. resource_group_automation.py +152 -0
  2. son-of-a-nutcracker.md +74 -0
  3. test.py +191 -0
resource_group_automation.py ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+ from environs import Env
3
+
4
+ # Initialize environment
5
+ env = Env()
6
+ env.read_env() # Looks for .env files in current directory
7
+
8
+ # Set your token (in a real environment, get this from env vars)
9
+ HF_TOKEN = env("HF_TOKEN")
10
+ ORG_NAME = env("ORG_NAME")
11
+ BASE_URL = env("BASE_URL")
12
+
13
+ # HTTP Headers for API requests
14
+ headers = {
15
+ "Authorization": f"Bearer {HF_TOKEN}",
16
+ "Content-Type": "application/json"
17
+ }
18
+
19
+ # First thing's first: create the repo (or use existing one)
20
+ def create_repository(org_name, repo_name):
21
+ """Create repository or use existing one"""
22
+ # First check if it already exists
23
+ repo_url = f"{BASE_URL}/models/{org_name}/{repo_name}"
24
+ check_response = requests.get(repo_url, headers=headers)
25
+
26
+ if check_response.status_code == 200:
27
+ print(f"Good news! Repository {org_name}/{repo_name} already exists!")
28
+ return True
29
+
30
+ # Try to create it if it doesn't exist
31
+ create_url = f"{BASE_URL}/repos/create"
32
+ print(f"Houston, stand by: Creating repo with URL: {create_url}")
33
+ data = {
34
+ "type": "model",
35
+ "name": repo_name,
36
+ "organization": org_name,
37
+ "private": True
38
+ }
39
+
40
+ response = requests.post(create_url, headers=headers, json=data)
41
+ if response.status_code == 200:
42
+ print(f"Woohoo!!! Created repository: {org_name}/{repo_name}")
43
+ return True
44
+ else:
45
+ # Check if the error is because the repo already exists
46
+ error_data = response.json()
47
+ if "error" in error_data and "already created" in error_data["error"]:
48
+ print(f"Repository {org_name}/{repo_name} already exists! Moving on.")
49
+ return True
50
+ else:
51
+ print(f"Son of a Lambda Cold Start! Failed to create repository: {response.text}")
52
+ return False
53
+
54
+ def create_resource_group_for_repo(repo_name, creator_username):
55
+ """
56
+ The magical function that creates a resource group for a new repository.
57
+ """
58
+ # Step 0: Make the repo first or we're going nowhere fast
59
+ if not create_repository(ORG_NAME, repo_name):
60
+ print("Hey, yo! Slow your roll! Can't make a resource group for a repo that doesn't exist!")
61
+ return False
62
+
63
+ # Step 1: Create our glorious resource group
64
+ rg_name = f"rg-{repo_name}" # Simpler name, less chance for API to throw a fit
65
+ rg_url = f"{BASE_URL}/organizations/{ORG_NAME}/resource-groups"
66
+ print(f"Conjuring a resource group with URL: {rg_url}")
67
+
68
+ # Let's be direct - just create it with minimal data
69
+ data = {
70
+ "name": rg_name,
71
+ "description": f"Automagically created for {repo_name} by the Wizard of Automation"
72
+ }
73
+
74
+ # Throw the request into the void and pray for a 200 or 201
75
+ response = requests.post(rg_url, headers=headers, json=data)
76
+
77
+ # Print full response for debugging (you know it will happen)
78
+ print(f"Resource group creation response: {response.status_code} - {response.text}")
79
+
80
+ success = False
81
+ if response.status_code == 201 or response.status_code == 200:
82
+ try:
83
+ response_data = response.json()
84
+ if "id" in response_data:
85
+ success = True
86
+ rg_id = response_data.get("id")
87
+ print(f"Resource group successfully summoned! ID: {rg_id}")
88
+ else:
89
+ print("Response has unexpected format - missing 'id' field")
90
+ except Exception as e:
91
+ print(f"Error parsing response: {e}")
92
+
93
+ if not success:
94
+ print(f"Son of a Nutcracker! Resource group creation failed: {response.text}")
95
+ return False
96
+
97
+ # We're in business! Grab that ID
98
+ rg_id = response.json().get("id")
99
+ print(f"I stole, I mean grabbed that Resource Group ID because I need it: {rg_id}")
100
+
101
+ # Step 2: Connect the repo to our shiny new resource group
102
+ repo_url = f"{BASE_URL}/models/{ORG_NAME}/{repo_name}/resource-group"
103
+ repo_data = {
104
+ "resourceGroupId": rg_id
105
+ }
106
+
107
+ repo_response = requests.post(repo_url, headers=headers, json=repo_data)
108
+
109
+ if repo_response.status_code != 200:
110
+ print(f"Son of an API! Failed to connect repo: {repo_response.text}")
111
+ return False
112
+
113
+ print(f"Repository {repo_name} has joined the resource group party!")
114
+
115
+ # Step 3: Make the creator an admin - they brought this repo into the world after all
116
+ member_url = f"{BASE_URL}/organizations/{ORG_NAME}/resource-groups/{rg_id}/users"
117
+ member_data = {
118
+ "users": [
119
+ {
120
+ "user": creator_username,
121
+ "role": "admin"
122
+ }
123
+ ]
124
+ }
125
+
126
+ member_response = requests.post(member_url, headers=headers, json=member_data)
127
+
128
+ if member_response.status_code != 200:
129
+ print(f"Son of a broken websocket! Creator couldn't be added: {member_response.text}")
130
+ # We'll continue anyway - the group and repo connection are working
131
+ else:
132
+ print(f"Creator {creator_username} is now the admin overlord of this resource group!")
133
+
134
+ # Victory dance
135
+ return True
136
+
137
+ # Demo time: This simulates what would happen when a new repo is created
138
+
139
+ def main():
140
+ repo_name = "testing-magic"
141
+ creator = "joshhayles"
142
+
143
+ print(f"A WIZARD created a new Repo: {ORG_NAME}/{repo_name}")
144
+ result = create_resource_group_for_repo(repo_name, creator)
145
+
146
+ if result:
147
+ print("Ohh, snap! Successfully set up resource group for the new repository!")
148
+ else:
149
+ print("Damn. Failed to set up resource group")
150
+
151
+ if __name__ == "__main__":
152
+ main()
son-of-a-nutcracker.md ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Automation Nutcracker: HF-ROCKS Edition
2
+
3
+ Featuring:
4
+ - A Wizard,
5
+ - a token,
6
+ - a nutcracker
7
+ - and Oscar the Grouch
8
+
9
+ How I walked into a terminal and got jack-hammered slapped with errors. And please don't ask me what jack-hammered slapped means. Nobody knows.
10
+
11
+ ## Symptoms
12
+ - Terminal vomit featuring `Son of a Nutcracker`
13
+ - API responses colder than a Lambda start
14
+ - The lingering suspicion that the machine knows you're trying to be cool :sunglasses:
15
+
16
+ ---
17
+
18
+ ## Cause of Death
19
+ - **Ego-Driven Org Name Change**: Trying to flex by renaming the org to `HF-ROCKS` instead of just leaving `eh-quizz` alone.
20
+ - **Token Permissions in Witness Protection**: The API didn't lie; my token was missing org-level permissions. But it still pissed me off.
21
+ - **Blind Faith in the UI**: Thinking that what I saw in the Hugging Face dashboard actually **mattered** in the API response.
22
+
23
+ ---
24
+
25
+ ## Autopsy Findings
26
+ Here's how it all unraveled:
27
+
28
+ ### The First Heartbreak
29
+ ```
30
+ A WIZARD created a new Repo: HF ROCKS/testing-magic
31
+ Son of a Nutcracker. Failed to create resource group: {"error":"Sorry, we can't find the page you are looking for."}
32
+ Damn. Failed to set up resource group
33
+ ```
34
+
35
+ Conclusion: The API didn't give a flying horse monkey crap about my cool new org name.
36
+
37
+ And once again, please don't ask me what flying horse monkey crap means. Nobody knows.
38
+
39
+ ---
40
+
41
+ ### Forensics Report
42
+ After scraping away the layers with `test.py`, I finally hit paydirt:
43
+ ```json
44
+ "orgs": [{"type":"org","id":"67a287f99b8fb9f109323d45","name":"eh-quizz","fullname":"HF-ROCKS",...}]
45
+ ```
46
+ **Mismatched `name` field detected.**
47
+
48
+ My whole plan went straight into Oscar the Grouch's trashcan because the API was locked on the original name, not the flashy one I gave it in the UI.
49
+
50
+ ---
51
+
52
+ ### Resurrection Steps
53
+ 1. Update token permissions to include `Org` scope (the hidden boss level nobody talks about).
54
+ 2. Accept the fact that API responses don't care about your branding ambitions.
55
+ 3. Run the script again...
56
+
57
+ ---
58
+
59
+ ## Golden Sunset
60
+ ```
61
+ Resource group successfully summoned! ID: 67c9ed9e658c9a336cb87170
62
+ Repository testing-magic has joined the resource group party!
63
+ Creator joshhayles is now the admin overlord of this resource group!
64
+ Ohh, snap! Successfully set up resource group for the new repository!
65
+ ```
66
+
67
+ Lessons learned?
68
+ - The API is always right.
69
+ - You're not cool enough to rename your org name mid-script.
70
+ - Automation without humility will absolutely clown you.
71
+
72
+ ---
73
+
74
+ **Autopsy Complete**
test.py ADDED
@@ -0,0 +1,191 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # I need to print some stuff out so I know what I'm dealing with here
2
+ # Go ahead, run the file. I DARE ya!
3
+
4
+ import requests
5
+ import json
6
+ from environs import Env
7
+
8
+ # Initialize environment
9
+ env = Env()
10
+ env.read_env()
11
+
12
+ HF_TOKEN = env("HF_TOKEN")
13
+ ORG_NAME = env("ORG_NAME")
14
+ BASE_URL = "https://huggingface.co/api"
15
+
16
+ headers = {
17
+ "Authorization": f"Bearer {HF_TOKEN}",
18
+ "Content-Type": "application/json"
19
+ }
20
+
21
+ def print_section(title):
22
+ """Print a simple section header"""
23
+ print("\n" + "=" * 50)
24
+ print(title)
25
+ print("=" * 50)
26
+
27
+ def format_response(label, response):
28
+ """Format and print JSON response with clear structure"""
29
+ print(f"\n{label}: {response.status_code}")
30
+
31
+ if response.status_code == 200:
32
+ try:
33
+ # Parse and return the JSON data
34
+ data = response.json()
35
+ # Format and print it with indentation
36
+ formatted_json = json.dumps(data, indent=2)
37
+ print(formatted_json)
38
+ return data
39
+ except json.JSONDecodeError:
40
+ print("Error: Could not parse JSON response")
41
+ print(response.text)
42
+ return None
43
+ else:
44
+ print(f"Error: {response.text}")
45
+ return None
46
+
47
+ # First, check authentication and user info
48
+ print_section("AUTHENTICATION CHECK")
49
+ org_url = f"{BASE_URL}/whoami-v2"
50
+ response = requests.get(org_url, headers=headers)
51
+ user_data = format_response("Whoami response", response)
52
+
53
+ # List existing resource groups
54
+ print_section("RESOURCE GROUPS")
55
+ list_rg_url = f"{BASE_URL}/organizations/{ORG_NAME}/resource-groups"
56
+ response = requests.get(list_rg_url, headers=headers)
57
+ resource_groups = format_response("List resource groups response", response)
58
+
59
+ # Create a test resource group only if it doesn't already exist
60
+ print_section("RESOURCE GROUP CREATION TEST")
61
+ new_group_name = "test-resource-group"
62
+ new_group_description = "A test resource group"
63
+
64
+ # Check if a group with this name already exists
65
+ group_exists = False
66
+ if resource_groups:
67
+ for group in resource_groups:
68
+ if group.get("name") == new_group_name:
69
+ group_exists = True
70
+ print(f"\nResource group '{new_group_name}' already exists with ID: {group.get('id')}")
71
+ print("Skipping creation to avoid duplicates.")
72
+ break
73
+
74
+ # Only create the group if it doesn't exist
75
+ if not group_exists:
76
+ create_rg_url = f"{BASE_URL}/organizations/{ORG_NAME}/resource-groups"
77
+ data = {
78
+ "name": new_group_name,
79
+ "description": new_group_description
80
+ }
81
+ response = requests.post(create_rg_url, headers=headers, json=data)
82
+ format_response("Create resource group response", response)
83
+
84
+ """
85
+ Expected response:
86
+
87
+ ==================================================
88
+ AUTHENTICATION CHECK
89
+ ==================================================
90
+
91
+ Whoami response: 200
92
+ {
93
+ "type": "user",
94
+ "id": "67c8834889772d508b6fa33c",
95
+ "name": "joshhayles",
96
+ "fullname": "Josh Hayles",
97
+ "isPro": false,
98
+ "avatarUrl": "https://cdn-avatars.huggingface.co/v1/production/uploads/67c8834889772d508b6fa33c/oZqi8zNQsTCSunm5NFnhE.png",
99
+ "orgs": [
100
+ {
101
+ "type": "org",
102
+ "id": "67a287f99b8fb9f109323d45",
103
+ "name": "eh-quizz",
104
+ "fullname": "eh-quizz",
105
+ "email": "[email protected]",
106
+ "canPay": false,
107
+ "periodEnd": 1743465599,
108
+ "avatarUrl": "https://cdn-avatars.huggingface.co/v1/production/uploads/67c8834889772d508b6fa33c/dU5WKVWLSq0jaYjG-zJBg.png",
109
+ "roleInOrg": "admin",
110
+ "isEnterprise": true
111
+ }
112
+ ],
113
+ "auth": {
114
+ "type": "access_token",
115
+ "accessToken": {
116
+ "displayName": "wuzzup-token",
117
+ "role": "fineGrained",
118
+ "createdAt": "2025-03-05T23:19:18.771Z",
119
+ "fineGrained": {
120
+ "canReadGatedRepos": true,
121
+ "global": [
122
+ "inference.serverless.write",
123
+ "discussion.write",
124
+ "post.write"
125
+ ],
126
+ "scoped": [
127
+ {
128
+ "entity": {
129
+ "_id": "67c9e5b9b98e0e0b6605992f",
130
+ "type": "model",
131
+ "name": "eh-quizz/testing-magic"
132
+ },
133
+ "permissions": [
134
+ "repo.content.read",
135
+ "repo.write"
136
+ ]
137
+ },
138
+ {
139
+ "entity": {
140
+ "_id": "67a287f99b8fb9f109323d45",
141
+ "type": "org",
142
+ "name": "eh-quizz"
143
+ },
144
+ "permissions": [
145
+ "repo.content.read",
146
+ "discussion.write",
147
+ "repo.write",
148
+ "org.read",
149
+ "org.write",
150
+ "resourceGroup.write"
151
+ ]
152
+ },
153
+ {
154
+ "entity": {
155
+ "_id": "67c8834889772d508b6fa33c",
156
+ "type": "user",
157
+ "name": "joshhayles"
158
+ },
159
+ "permissions": [
160
+ "repo.content.read",
161
+ "inference.endpoints.infer.write",
162
+ "user.webhooks.read",
163
+ "discussion.write"
164
+ ]
165
+ }
166
+ ]
167
+ }
168
+ }
169
+ }
170
+ }
171
+
172
+ ==================================================
173
+ RESOURCE GROUPS
174
+ ==================================================
175
+
176
+ List resource groups response: 200
177
+ []
178
+
179
+ ==================================================
180
+ RESOURCE GROUP CREATION TEST
181
+ ==================================================
182
+
183
+ Create resource group response: 200
184
+ {
185
+ "id": "67ca1e89dd6c6628fcb2ca6d",
186
+ "name": "test-resource-group",
187
+ "description": "A test resource group",
188
+ "users": [],
189
+ "repos": []
190
+ }
191
+ """