Upload 3 files
Browse files- resource_group_automation.py +152 -0
- son-of-a-nutcracker.md +74 -0
- 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 |
+
"""
|