gebsunamy commited on
Commit
cc0d6b8
·
verified ·
1 Parent(s): 8c3d56d

Upload folder using huggingface_hub

Browse files
Files changed (2) hide show
  1. main.py +99 -115
  2. test_offline.py +70 -0
main.py CHANGED
@@ -5,10 +5,11 @@ import json
5
  from datetime import datetime
6
  from typing import List, Optional, Dict, Any
7
 
8
- from fastapi import FastAPI, HTTPException, Request
9
  from fastapi.responses import HTMLResponse
10
  from fastapi.middleware.cors import CORSMiddleware
11
  from huggingface_hub import HfApi, hf_hub_download, login
 
12
 
13
  # Setup logging
14
  logging.basicConfig(level=logging.INFO)
@@ -17,21 +18,30 @@ logger = logging.getLogger(__name__)
17
  # Configuration
18
  TOKEN = os.getenv("HF_TOKEN")
19
  REPO_ID = "gebsunamy/MemoryAndSkills_Data"
 
20
 
21
- if TOKEN:
22
- login(token=TOKEN)
23
-
24
- api = HfApi()
25
-
26
- # Ensure the repository exists
27
- try:
28
- api.create_repo(repo_id=REPO_ID, repo_type="dataset", private=True)
29
- logger.info(f"Created private dataset repository: {REPO_ID}")
30
- except Exception:
31
- pass
 
 
32
 
33
- # --- DATA HELPERS (JSON) ---
34
  def load_json_data(file_name: str) -> List[Dict]:
 
 
 
 
 
 
35
  try:
36
  path = hf_hub_download(
37
  repo_id=REPO_ID,
@@ -42,15 +52,22 @@ def load_json_data(file_name: str) -> List[Dict]:
42
  with open(path, "r", encoding="utf-8") as f:
43
  return json.load(f)
44
  except Exception as e:
45
- logger.warning(f"File {file_name} not found or error: {e}. Returning empty list.")
46
  return []
47
 
48
  def save_json_data(data: List[Dict], file_name: str):
 
 
 
 
 
 
49
  try:
50
  temp_file = f"temp_{file_name}"
51
  with open(temp_file, "w", encoding="utf-8") as f:
52
  json.dump(data, f, ensure_ascii=False, indent=2)
53
 
 
54
  api.upload_file(
55
  path_or_fileobj=temp_file,
56
  path_in_repo=file_name,
@@ -59,27 +76,31 @@ def save_json_data(data: List[Dict], file_name: str):
59
  token=TOKEN
60
  )
61
  os.remove(temp_file)
62
- logger.info(f"Successfully uploaded {file_name} to HF Hub.")
63
  except Exception as e:
64
  logger.error(f"Failed to save {file_name}: {e}")
65
 
 
 
 
 
 
 
 
 
 
66
  # --- SUB-APPS ---
67
 
68
  # 1. MEMORY APP
69
- memory_app = FastAPI(title="Memory API", version="2.0.0")
70
 
71
- @memory_app.get("/create")
72
- async def create_memory(text: str, metadata: str = "{}"):
73
  data = load_json_data("memories.json")
74
- try:
75
- meta_dict = json.loads(metadata)
76
- except:
77
- meta_dict = {"raw": metadata}
78
-
79
  new_item = {
80
  "id": str(uuid.uuid4()),
81
- "text": text,
82
- "metadata": meta_dict,
83
  "timestamp": datetime.utcnow().isoformat()
84
  }
85
  data.append(new_item)
@@ -90,14 +111,6 @@ async def create_memory(text: str, metadata: str = "{}"):
90
  async def get_all_memories():
91
  return load_json_data("memories.json")
92
 
93
- @memory_app.get("/get_once")
94
- async def get_memory(id: str):
95
- data = load_json_data("memories.json")
96
- item = next((m for m in data if m["id"] == id), None)
97
- if not item:
98
- raise HTTPException(status_code=404, detail="Memory not found")
99
- return item
100
-
101
  @memory_app.get("/search")
102
  async def search_memories(query: str):
103
  data = load_json_data("memories.json")
@@ -112,15 +125,15 @@ async def delete_memory(id: str):
112
 
113
 
114
  # 2. SKILLS APP
115
- skills_app = FastAPI(title="Skills API", version="2.0.0")
116
 
117
- @skills_app.get("/create")
118
- async def create_skill(skill_name: str, skill_content: str):
119
  data = load_json_data("skills.json")
120
  new_item = {
121
  "skill_id": str(uuid.uuid4()),
122
- "skill_name": skill_name,
123
- "skill_content": skill_content,
124
  "timestamp": datetime.utcnow().isoformat()
125
  }
126
  data.append(new_item)
@@ -162,51 +175,39 @@ app.mount("/memory", memory_app)
162
  app.mount("/skills", skills_app)
163
 
164
  @app.get("/", response_class=HTMLResponse)
165
- async def home(request: Request):
 
166
  return f"""
167
  <html>
168
  <head>
169
- <title>Memory & Skills Hub</title>
170
  <style>
171
  body {{ font-family: -apple-system, sans-serif; max-width: 900px; margin: 40px auto; background: #f8f9fa; color: #212529; padding: 20px; }}
172
  .container {{ background: white; padding: 30px; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.08); }}
173
- h1 {{ color: #0d6efd; border-bottom: 2px solid #0d6efd; padding-bottom: 10px; }}
 
174
  .section {{ margin-top: 30px; padding: 20px; border: 1px solid #dee2e6; border-radius: 8px; }}
175
- .btn {{ display: inline-block; padding: 12px 24px; background: #0d6efd; color: white; text-decoration: none; border-radius: 6px; font-weight: bold; margin: 5px; transition: opacity 0.2s; }}
176
- .btn-green {{ background: #198754; }}
177
- .btn:hover {{ opacity: 0.85; }}
178
- code {{ background: #e9ecef; padding: 3px 6px; border-radius: 4px; font-size: 0.9em; }}
179
- .links {{ margin-top: 10px; }}
180
- .links a {{ color: #6c757d; font-size: 0.85em; margin-right: 15px; }}
181
  </style>
182
  </head>
183
  <body>
184
  <div class="container">
185
- <h1>🧠 Memory & Skills Hub (JSON Engine)</h1>
186
- <p>Universal <code>GET</code>-only storage for AI. Data is persisted as JSON on Hugging Face.</p>
187
 
188
  <div class="section">
189
  <h3>📂 Memories</h3>
190
- <p>Manage facts, history, and observations.</p>
191
- <a class="btn" href="/memory/docs">Swagger Docs</a>
192
- <div class="links">
193
- <a href="/memory/openapi.json">openapi.json</a>
194
- </div>
195
  </div>
196
 
197
  <div class="section">
198
  <h3>🛠 Skills</h3>
199
- <p>Store specialized knowledge and capabilities.</p>
200
- <a class="btn" href="/skills/docs">Swagger Docs</a>
201
- <div class="links">
202
- <a href="/skills/openapi.json">openapi.json</a>
203
- </div>
204
  </div>
205
 
206
  <div class="section">
207
- <h3>⚙️ Control Panel</h3>
208
- <p>Add or remove data via a web interface.</p>
209
- <a class="btn btn-green" href="/admin">Open Admin Dashboard</a>
210
  </div>
211
  </div>
212
  </body>
@@ -214,20 +215,17 @@ async def home(request: Request):
214
  """
215
 
216
  @app.get("/admin", response_class=HTMLResponse)
217
- async def admin_dashboard(request: Request):
218
  return f"""
219
  <html>
220
  <head>
221
  <title>Admin Dashboard</title>
222
  <style>
223
- body {{ font-family: sans-serif; padding: 20px; background: #f4f4f9; color: #333; }}
224
  .card {{ background: white; padding: 20px; margin-bottom: 25px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.05); }}
225
  input, textarea {{ width: 100%; margin: 10px 0; padding: 12px; border: 1px solid #ddd; border-radius: 5px; box-sizing: border-box; }}
226
- button {{ background: #0d6efd; color: white; border: none; padding: 12px 20px; cursor: pointer; border-radius: 5px; font-weight: bold; }}
227
- button.delete {{ background: #dc3545; padding: 5px 10px; font-size: 0.8em; }}
228
- .item-row {{ border-bottom: 1px solid #eee; padding: 10px 0; display: flex; justify-content: space-between; align-items: center; }}
229
- .item-info {{ flex-grow: 1; }}
230
- h2 {{ margin-top: 0; color: #0d6efd; }}
231
  </style>
232
  </head>
233
  <body>
@@ -236,77 +234,63 @@ async def admin_dashboard(request: Request):
236
 
237
  <div class="card">
238
  <h2>➕ New Memory</h2>
239
- <input id="m_text" placeholder="What should I remember?">
240
- <button onclick="addMemory()">Save Memory</button>
241
  </div>
242
 
243
  <div class="card">
244
  <h2>➕ New Skill</h2>
245
- <input id="s_name" placeholder="Skill Name (e.g. Python Programming)">
246
- <textarea id="s_content" placeholder="Skill Content / Details..." rows="3"></textarea>
247
- <button onclick="addSkill()">Save Skill</button>
248
  </div>
249
 
250
  <div class="card">
251
  <h2>📦 Managed Data</h2>
252
  <button onclick="loadData()">🔄 Refresh</button>
253
- <div id="status"></div>
254
- <h3>Memories</h3>
255
- <div id="memories">Loading...</div>
256
- <h3>Skills</h3>
257
- <div id="skills">Loading...</div>
258
  </div>
259
 
260
  <script>
261
  async function addMemory() {{
262
  const text = document.getElementById('m_text').value;
263
- if(!text) return;
264
- await fetch(`/memory/create?text=${{encodeURIComponent(text)}}`);
 
 
 
265
  document.getElementById('m_text').value = '';
266
  loadData();
267
  }}
268
  async function addSkill() {{
269
- const name = document.getElementById('s_name').value;
270
- const content = document.getElementById('s_content').value;
271
- if(!name || !content) return;
272
- await fetch(`/skills/create?skill_name=${{encodeURIComponent(name)}}&skill_content=${{encodeURIComponent(content)}}`);
 
 
 
273
  document.getElementById('s_name').value = '';
274
  document.getElementById('s_content').value = '';
275
  loadData();
276
  }}
277
- async function deleteMemory(id) {{
278
- if(!confirm('Delete this memory?')) return;
279
- await fetch(`/memory/delete?id=${{id}}`);
280
- loadData();
281
- }}
282
- async function deleteSkill(id) {{
283
- if(!confirm('Delete this skill?')) return;
284
- await fetch(`/skills/delete?skill_id=${{id}}`);
285
  loadData();
286
  }}
287
  async function loadData() {{
288
- try {{
289
- const mRes = await fetch('/memory/get_all');
290
- const mData = await mRes.json();
291
- document.getElementById('memories').innerHTML = mData.map(m => `
292
- <div class="item-row">
293
- <div class="item-info">${{m.text}}</div>
294
- <button class="delete" onclick="deleteMemory('${{m.id}}')">Delete</button>
295
- </div>
296
- `).join('') || 'No memories found.';
297
-
298
- const sRes = await fetch('/skills/get_all');
299
- const sData = await sRes.json();
300
- document.getElementById('skills').innerHTML = sData.map(s => `
301
- <div class="item-row">
302
- <div class="item-info"><b>${{s.skill_name}}</b>: ${{s.skill_content}}</div>
303
- <button class="delete" onclick="deleteSkill('${{s.skill_id}}')">Delete</button>
304
- </div>
305
- `).join('') || 'No skills found.';
306
- }} catch (err) {{
307
- console.error('Error loading data:', err);
308
- document.getElementById('status').innerText = 'Error loading data.';
309
- }}
310
  }}
311
  window.onload = loadData;
312
  </script>
 
5
  from datetime import datetime
6
  from typing import List, Optional, Dict, Any
7
 
8
+ from fastapi import FastAPI, HTTPException, Request, Body
9
  from fastapi.responses import HTMLResponse
10
  from fastapi.middleware.cors import CORSMiddleware
11
  from huggingface_hub import HfApi, hf_hub_download, login
12
+ from pydantic import BaseModel
13
 
14
  # Setup logging
15
  logging.basicConfig(level=logging.INFO)
 
18
  # Configuration
19
  TOKEN = os.getenv("HF_TOKEN")
20
  REPO_ID = "gebsunamy/MemoryAndSkills_Data"
21
+ OFFLINE_MODE = TOKEN is None
22
 
23
+ if not OFFLINE_MODE:
24
+ try:
25
+ login(token=TOKEN)
26
+ api = HfApi()
27
+ # Ensure the repository exists
28
+ try:
29
+ api.create_repo(repo_id=REPO_ID, repo_type="dataset", private=True)
30
+ logger.info(f"Connected to HF: {REPO_ID}")
31
+ except Exception:
32
+ pass
33
+ except Exception as e:
34
+ logger.error(f"HF Login failed: {e}. Switching to OFFLINE MODE.")
35
+ OFFLINE_MODE = True
36
 
37
+ # --- DATA HELPERS (JSON + LOCAL FALLBACK) ---
38
  def load_json_data(file_name: str) -> List[Dict]:
39
+ if OFFLINE_MODE:
40
+ if os.path.exists(file_name):
41
+ with open(file_name, "r", encoding="utf-8") as f:
42
+ return json.load(f)
43
+ return []
44
+
45
  try:
46
  path = hf_hub_download(
47
  repo_id=REPO_ID,
 
52
  with open(path, "r", encoding="utf-8") as f:
53
  return json.load(f)
54
  except Exception as e:
55
+ logger.warning(f"File {file_name} not found on HF: {e}. Returning empty list.")
56
  return []
57
 
58
  def save_json_data(data: List[Dict], file_name: str):
59
+ if OFFLINE_MODE:
60
+ with open(file_name, "w", encoding="utf-8") as f:
61
+ json.dump(data, f, ensure_ascii=False, indent=2)
62
+ logger.info(f"Saved {file_name} locally.")
63
+ return
64
+
65
  try:
66
  temp_file = f"temp_{file_name}"
67
  with open(temp_file, "w", encoding="utf-8") as f:
68
  json.dump(data, f, ensure_ascii=False, indent=2)
69
 
70
+ api = HfApi()
71
  api.upload_file(
72
  path_or_fileobj=temp_file,
73
  path_in_repo=file_name,
 
76
  token=TOKEN
77
  )
78
  os.remove(temp_file)
79
+ logger.info(f"Uploaded {file_name} to HF Hub.")
80
  except Exception as e:
81
  logger.error(f"Failed to save {file_name}: {e}")
82
 
83
+ # --- SCHEMAS ---
84
+ class MemoryCreate(BaseModel):
85
+ text: str
86
+ metadata: Optional[Dict] = {}
87
+
88
+ class SkillCreate(BaseModel):
89
+ skill_name: str
90
+ skill_content: str
91
+
92
  # --- SUB-APPS ---
93
 
94
  # 1. MEMORY APP
95
+ memory_app = FastAPI(title="Memory API", version="3.0.0")
96
 
97
+ @memory_app.post("/create")
98
+ async def create_memory(item: MemoryCreate):
99
  data = load_json_data("memories.json")
 
 
 
 
 
100
  new_item = {
101
  "id": str(uuid.uuid4()),
102
+ "text": item.text,
103
+ "metadata": item.metadata,
104
  "timestamp": datetime.utcnow().isoformat()
105
  }
106
  data.append(new_item)
 
111
  async def get_all_memories():
112
  return load_json_data("memories.json")
113
 
 
 
 
 
 
 
 
 
114
  @memory_app.get("/search")
115
  async def search_memories(query: str):
116
  data = load_json_data("memories.json")
 
125
 
126
 
127
  # 2. SKILLS APP
128
+ skills_app = FastAPI(title="Skills API", version="3.0.0")
129
 
130
+ @skills_app.post("/create")
131
+ async def create_skill(item: SkillCreate):
132
  data = load_json_data("skills.json")
133
  new_item = {
134
  "skill_id": str(uuid.uuid4()),
135
+ "skill_name": item.skill_name,
136
+ "skill_content": item.skill_content,
137
  "timestamp": datetime.utcnow().isoformat()
138
  }
139
  data.append(new_item)
 
175
  app.mount("/skills", skills_app)
176
 
177
  @app.get("/", response_class=HTMLResponse)
178
+ async def home():
179
+ mode_text = "OFFLINE (Local Files)" if OFFLINE_MODE else "ONLINE (Hugging Face)"
180
  return f"""
181
  <html>
182
  <head>
183
+ <title>Memory Hub</title>
184
  <style>
185
  body {{ font-family: -apple-system, sans-serif; max-width: 900px; margin: 40px auto; background: #f8f9fa; color: #212529; padding: 20px; }}
186
  .container {{ background: white; padding: 30px; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.08); }}
187
+ .badge {{ background: #ffc107; padding: 5px 10px; border-radius: 5px; font-size: 0.8em; font-weight: bold; }}
188
+ h1 {{ color: #0d6efd; }}
189
  .section {{ margin-top: 30px; padding: 20px; border: 1px solid #dee2e6; border-radius: 8px; }}
190
+ .btn {{ display: inline-block; padding: 12px 24px; background: #0d6efd; color: white; text-decoration: none; border-radius: 6px; font-weight: bold; margin: 5px; }}
 
 
 
 
 
191
  </style>
192
  </head>
193
  <body>
194
  <div class="container">
195
+ <h1>🧠 Memory & Skills Hub <span class="badge">{mode_text}</span></h1>
196
+ <p>Universal storage. Creation uses <code>POST</code> (to handle large data), Reading uses <code>GET</code>.</p>
197
 
198
  <div class="section">
199
  <h3>📂 Memories</h3>
200
+ <a class="btn" href="/memory/docs">Docs</a>
 
 
 
 
201
  </div>
202
 
203
  <div class="section">
204
  <h3>🛠 Skills</h3>
205
+ <a class="btn" href="/skills/docs">Docs</a>
 
 
 
 
206
  </div>
207
 
208
  <div class="section">
209
+ <h3>⚙️ Admin</h3>
210
+ <a class="btn" style="background:#198754" href="/admin">Open Admin Dashboard</a>
 
211
  </div>
212
  </div>
213
  </body>
 
215
  """
216
 
217
  @app.get("/admin", response_class=HTMLResponse)
218
+ async def admin_dashboard():
219
  return f"""
220
  <html>
221
  <head>
222
  <title>Admin Dashboard</title>
223
  <style>
224
+ body {{ font-family: sans-serif; padding: 20px; background: #f4f4f9; }}
225
  .card {{ background: white; padding: 20px; margin-bottom: 25px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.05); }}
226
  input, textarea {{ width: 100%; margin: 10px 0; padding: 12px; border: 1px solid #ddd; border-radius: 5px; box-sizing: border-box; }}
227
+ button {{ background: #0d6efd; color: white; border: none; padding: 12px 20px; cursor: pointer; border-radius: 5px; }}
228
+ .item-row {{ border-bottom: 1px solid #eee; padding: 10px 0; display: flex; justify-content: space-between; }}
 
 
 
229
  </style>
230
  </head>
231
  <body>
 
234
 
235
  <div class="card">
236
  <h2>➕ New Memory</h2>
237
+ <input id="m_text" placeholder="Text...">
238
+ <button onclick="addMemory()">Save (POST)</button>
239
  </div>
240
 
241
  <div class="card">
242
  <h2>➕ New Skill</h2>
243
+ <input id="s_name" placeholder="Skill Name">
244
+ <textarea id="s_content" placeholder="Skill Content (Unlimited Size)..." rows="5"></textarea>
245
+ <button onclick="addSkill()">Save (POST)</button>
246
  </div>
247
 
248
  <div class="card">
249
  <h2>📦 Managed Data</h2>
250
  <button onclick="loadData()">🔄 Refresh</button>
251
+ <div id="memories"></div>
252
+ <div id="skills"></div>
 
 
 
253
  </div>
254
 
255
  <script>
256
  async function addMemory() {{
257
  const text = document.getElementById('m_text').value;
258
+ await fetch('/memory/create', {{
259
+ method: 'POST',
260
+ headers: {{ 'Content-Type': 'application/json' }},
261
+ body: JSON.stringify({{ text, metadata: {{}} }})
262
+ }});
263
  document.getElementById('m_text').value = '';
264
  loadData();
265
  }}
266
  async function addSkill() {{
267
+ const skill_name = document.getElementById('s_name').value;
268
+ const skill_content = document.getElementById('s_content').value;
269
+ await fetch('/skills/create', {{
270
+ method: 'POST',
271
+ headers: {{ 'Content-Type': 'application/json' }},
272
+ body: JSON.stringify({{ skill_name, skill_content }})
273
+ }});
274
  document.getElementById('s_name').value = '';
275
  document.getElementById('s_content').value = '';
276
  loadData();
277
  }}
278
+ async function deleteItem(type, id_param, id) {{
279
+ await fetch(`/${{type}}/delete?${{id_param}}=${{id}}`);
 
 
 
 
 
 
280
  loadData();
281
  }}
282
  async function loadData() {{
283
+ const mRes = await fetch('/memory/get_all');
284
+ const mData = await mRes.json();
285
+ document.getElementById('memories').innerHTML = '<h3>Memories</h3>' + mData.map(m => `
286
+ <div class="item-row"><span>${{m.text}}</span> <button onclick="deleteItem('memory', 'id', '${{m.id}}')">Del</button></div>
287
+ `).join('');
288
+
289
+ const sRes = await fetch('/skills/get_all');
290
+ const sData = await sRes.json();
291
+ document.getElementById('skills').innerHTML = '<h3>Skills</h3>' + sData.map(s => `
292
+ <div class="item-row"><span><b>${{s.skill_name}}</b></span> <button onclick="deleteItem('skills', 'skill_id', '${{s.skill_id}}')">Del</button></div>
293
+ `).join('');
 
 
 
 
 
 
 
 
 
 
 
294
  }}
295
  window.onload = loadData;
296
  </script>
test_offline.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import unittest
2
+ import os
3
+ import json
4
+ import time
5
+ from multiprocessing import Process
6
+ import uvicorn
7
+ import httpx
8
+ from main import app
9
+
10
+ PORT = 8001
11
+
12
+ def run_server():
13
+ uvicorn.run(app, host="127.0.0.1", port=PORT, log_level="error")
14
+
15
+ class TestOfflineHub(unittest.TestCase):
16
+ @classmethod
17
+ def setUpClass(cls):
18
+ if "HF_TOKEN" in os.environ:
19
+ del os.environ["HF_TOKEN"]
20
+
21
+ cls.server_process = Process(target=run_server)
22
+ cls.server_process.start()
23
+
24
+ # Wait for server to be ready
25
+ for _ in range(10):
26
+ try:
27
+ with httpx.Client() as client:
28
+ client.get(f"http://127.0.0.1:{PORT}/")
29
+ break
30
+ except:
31
+ time.sleep(1)
32
+ else:
33
+ cls.server_process.terminate()
34
+ raise RuntimeError("Server failed to start")
35
+
36
+ @classmethod
37
+ def tearDownClass(cls):
38
+ cls.server_process.terminate()
39
+ for f in ["memories.json", "skills.json"]:
40
+ if os.path.exists(f):
41
+ os.remove(f)
42
+
43
+ def test_memory_lifecycle(self):
44
+ url = f"http://127.0.0.1:{PORT}"
45
+ res = httpx.post(f"{url}/memory/create", json={"text": "Test Fact", "metadata": {}})
46
+ self.assertEqual(res.status_code, 200)
47
+ item_id = res.json()["id"]
48
+
49
+ res = httpx.get(f"{url}/memory/get_all")
50
+ self.assertEqual(len(res.json()), 1)
51
+
52
+ res = httpx.get(f"{url}/memory/search?query=Fact")
53
+ self.assertEqual(len(res.json()), 1)
54
+
55
+ res = httpx.get(f"{url}/memory/delete?id={item_id}")
56
+ self.assertEqual(res.json()["status"], "deleted")
57
+
58
+ def test_skills_lifecycle(self):
59
+ url = f"http://127.0.0.1:{PORT}"
60
+ large_content = "A" * 10000
61
+ res = httpx.post(f"{url}/skills/create", json={"skill_name": "Python", "skill_content": large_content})
62
+ self.assertEqual(res.status_code, 200)
63
+ skill_id = res.json()["skill_id"]
64
+
65
+ res = httpx.get(f"{url}/skills/get_once?skill_id={skill_id}")
66
+ self.assertEqual(res.json()["skill_name"], "Python")
67
+ self.assertEqual(len(res.json()["skill_content"]), 10000)
68
+
69
+ if __name__ == "__main__":
70
+ unittest.main()