Skip to content

Commit c8e131d

Browse files
authored
Merge branch 'main' into add_pgvectorscale
2 parents 6cb8146 + 40588dc commit c8e131d

105 files changed

Lines changed: 8538 additions & 449 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/upload-blogs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ on:
44
push:
55
branches:
66
- main
7+
workflow_dispatch:
78

89
jobs:
910
run-script:

blog/helpers.py

Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import os
22
import enum
33

4-
GIT_REPO = 'https://github.com/superlinked/VectorHub'
4+
GIT_REPO = "https://github.com/superlinked/VectorHub"
5+
56

67
class ItemType(enum.Enum):
78
FOLDER = "folder"
89
FILE = "file"
910

11+
1012
class Item:
1113
def __init__(self, type, name, path, has_blogs=False, children=None):
1214
self.type = type
@@ -36,7 +38,7 @@ def from_dict(cls, data):
3638
name=data.get("name", ""),
3739
path=data.get("path", ""),
3840
has_blogs=data.get("has_blogs", False),
39-
children=data.get("children", [])
41+
children=data.get("children", []),
4042
)
4143

4244
def to_dict(self):
@@ -62,27 +64,38 @@ def __init__(self, content, filepath, last_updated):
6264
self.title = self.get_title()
6365

6466
def get_title(self) -> str:
65-
lines = self.content.split('\n')
67+
lines = self.content.split("\n")
6668
first_line = str(lines[0]).strip()
67-
if first_line.startswith('# '):
68-
self.content = '\n'.join(lines[1:])
69+
if first_line.startswith("# "):
70+
self.content = "\n".join(lines[1:])
6971
self.content = self.content.strip()
70-
return first_line.replace('# ', '').strip()
72+
return first_line.replace("# ", "").strip()
7173
else:
72-
return os.path.basename(self.filepath).replace('-', ' ').replace('_', ' ').replace('.md', '')
74+
return (
75+
os.path.basename(self.filepath)
76+
.replace("-", " ")
77+
.replace("_", " ")
78+
.replace(".md", "")
79+
)
7380

7481
def __str__(self) -> str:
7582
return self.title
7683

7784
def get_github_url(self):
78-
return f'{GIT_REPO}/blob/main/{self.filepath}'
79-
85+
return f"{GIT_REPO}/blob/main/{self.filepath}"
86+
8087
def get_filepath(self):
81-
return self.filepath.replace('&', '').replace('--', '-').replace('__', '_')
82-
88+
return self.filepath.replace("&", "").replace("--", "-").replace("__", "_")
89+
8390
def get_slug(self):
8491
if not self.slug_url:
85-
slug = self.get_filepath().replace('.md', '').replace('_', '-').replace(' ', '-').replace('docs/', '')
92+
slug = (
93+
self.get_filepath()
94+
.replace(".md", "")
95+
.replace("_", "-")
96+
.replace(" ", "-")
97+
.replace("docs/", "")
98+
)
8699
self.slug_url = slug.lower()
87100
return self.slug_url
88101

@@ -94,17 +107,21 @@ def set_slug_url(self, slug_url):
94107

95108
def get_json(self):
96109
return {
97-
"github_url": self.get_github_url(),
98-
"content": self.content,
99-
"github_last_updated_date": self.last_updated,
100-
"title": self.title,
101-
"slug_url": self.get_slug(),
102-
"publishedAt": self.publishedAt,
103-
"filepath": self.get_filepath()
104-
}
110+
"github_url": self.get_github_url(),
111+
"content": self.content,
112+
"github_last_updated_date": self.last_updated,
113+
"title": self.title,
114+
"slug_url": self.get_slug(),
115+
"published_date": self.publishedAt,
116+
"filepath": self.get_filepath(),
117+
"meta_desc": self.meta_desc,
118+
}
105119

106120
def get_post_json(self, is_draft=False):
107121
return {"data": self.get_json()}
108122

109123
def __eq__(self, __value) -> bool:
110124
self.get_slug() == __value.get_slug()
125+
126+
def __str__(self):
127+
return f"StrapiBlog(title={self.title}, published_date={self.publishedAt})"

blog/main.py

Lines changed: 108 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -6,74 +6,83 @@
66
from urllib.parse import urljoin
77
from helpers import Item, ItemType, StrapiBlog
88
from tqdm.auto import tqdm
9-
from datetime import datetime
9+
from datetime import datetime, timezone
1010
from pathlib import Path
11+
from time import sleep
1112

1213
args = None
1314

14-
BASE_URL = os.getenv('STRAPI_URL', "")
15-
API_KEY = os.getenv('STRAPI_API_KEY', "")
15+
BASE_URL = os.getenv("STRAPI_URL", "")
16+
API_KEY = os.getenv("STRAPI_API_KEY", "")
1617

1718
paths_to_search = []
1819
existing_filepaths_discovered = {}
1920

20-
headers = {
21-
'Authorization': f'Bearer {API_KEY}',
22-
'Content-Type': 'application/json'
23-
}
21+
headers = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}
22+
2423

2524
def arg_parse():
2625
global args
2726
parser = argparse.ArgumentParser(description="VectorHub Strapi Upload")
28-
parser.add_argument('--directories', help='Path to json which describes the directories to parse')
27+
parser.add_argument(
28+
"--directories", help="Path to json which describes the directories to parse"
29+
)
2930
args = parser.parse_args()
3031

32+
3133
def load_items_from_json(directories: str) -> list:
3234
if os.path.exists(directories):
33-
items = []
3435
try:
35-
with open(directories, 'r') as file:
36+
with open(directories, "r") as file:
3637
data = json.load(file)
37-
for item_data in data:
38-
items.append(Item.from_dict(item_data))
39-
except JSONDecodeError as e:
40-
print('JSON Structure is invalid.')
38+
return [Item.from_dict(item_data) for item_data in data]
39+
except JSONDecodeError:
40+
print("❌ Invalid JSON structure.")
4141
exit(1)
4242
except Exception as e:
43-
print('Unknown error occured.')
43+
print("❌ Unknown error while reading directory JSON:")
4444
print(e)
4545
exit(1)
46-
return items
4746
else:
4847
print(f"{directories} does not exist.")
4948
exit(1)
5049

5150

5251
def load_existing_blogs(page_num=1):
52+
"""Loads all blogs currently in Strapi."""
5353
global existing_filepaths_discovered
54-
base_url = urljoin(BASE_URL, 'api/blogs')
55-
search_url = base_url + f"?pagination[page]={page_num}&publicationState=preview"
5654

57-
session = requests.Session()
55+
base_url = urljoin(BASE_URL, "api/blogs")
56+
search_url = f"{base_url}?pagination[page]={page_num}&pagination[pageSize]=100"
5857

58+
session = requests.Session()
5959
response = session.get(search_url, headers=headers)
60+
6061
if response.status_code == 200:
61-
data = json.loads(response.text)['data']
62-
if len(data) > 0:
63-
for item in data:
64-
existing_filepaths_discovered[item['attributes']['filepath']] = {'discovered': False, 'id': item['id']}
65-
load_existing_blogs(page_num+1)
62+
data = response.json().get("data", [])
63+
if not data:
64+
return
65+
for item in data:
66+
filepath = item.get("filepath")
67+
if filepath:
68+
existing_filepaths_discovered[filepath] = {
69+
"discovered": False,
70+
"id": item["id"],
71+
}
72+
load_existing_blogs(page_num + 1)
73+
else:
74+
print(f"⚠️ Failed to load blogs: {response.status_code} {response.text}")
6675

6776

6877
def fetch_paths(node: Item, current_path=""):
78+
"""Recursively collect directories containing blogs."""
6979
global paths_to_search
70-
# Update the current path with dthe node's path
80+
7181
current_path = f"{current_path}/{node.path}" if current_path else node.path
7282

73-
# If the node has children, recurse on each child
7483
if node.has_blogs:
7584
paths_to_search.append(current_path)
76-
if node.children and len(node.children) > 0:
85+
if node.children:
7786
for child in node.children:
7887
fetch_paths(child, current_path)
7988

@@ -85,78 +94,102 @@ def find_files_to_upload(items: list):
8594
fetch_paths(item)
8695

8796
files = []
97+
extension = "md"
8898

89-
extension = 'md'
90-
9199
for path in paths_to_search:
92100
folder_path = Path(path)
93-
folder_files = folder_path.glob(f"*.{extension}")
94-
for file in folder_files:
95-
if 'readme.md' not in str(file).lower():
96-
files.append({
97-
'path': str(file),
98-
'time': datetime.fromtimestamp(os.path.getmtime(file)).strftime("%Y-%m-%d")
99-
})
100-
101+
for file in folder_path.glob(f"*.{extension}"):
102+
if "readme.md" not in str(file).lower():
103+
files.append(
104+
{
105+
"path": str(file),
106+
"time": datetime.fromtimestamp(os.path.getmtime(file)).strftime(
107+
"%Y-%m-%d"
108+
),
109+
}
110+
)
101111
return files
102112

103113

104114
def build_blog_object(file_obj: dict) -> StrapiBlog:
105-
filepath = file_obj['path']
106-
with open(filepath, 'r') as file:
115+
filepath = file_obj["path"]
116+
with open(filepath, "r") as file:
107117
content = file.read()
108-
blog = StrapiBlog(content, filepath, file_obj['time'])
109-
return blog
118+
return StrapiBlog(content, filepath, file_obj["time"])
119+
110120

111121
def upload_blog(blog: StrapiBlog):
112-
base_url = urljoin(BASE_URL, 'api/blogs')
122+
"""Uploads or updates a blog to Strapi v5."""
123+
base_url = urljoin(BASE_URL, "api/blogs")
113124
filepath = blog.get_filepath()
114-
search_url = base_url + f"?filters[filepath][$eqi]={filepath}&publicationState=preview"
125+
search_url = f"{base_url}?filters[filepath][$eqi]={filepath}"
126+
115127
session = requests.Session()
116128

117129
if filepath in existing_filepaths_discovered:
118-
existing_filepaths_discovered[filepath]['discovered'] = True
130+
existing_filepaths_discovered[filepath]["discovered"] = True
119131

120132
response = session.get(search_url, headers=headers)
133+
if response.status_code != 200:
134+
print(f"❌ Error fetching blog {filepath}: {response.text}")
135+
return
121136

122-
if response.status_code == 200:
123-
responses = json.loads(response.text)['data']
124-
print(f'Uploading filepath: {blog.get_filepath()}')
125-
if len(responses) > 0:
126-
# Blog already exists at this filepath
127-
id = json.loads(response.text)['data'][0]['id']
137+
existing = response.json().get("data", [])
138+
print(f"📤 Uploading filepath: {filepath}")
128139

129-
blog.set_slug_url(json.loads(response.text)['data'][0]['attributes']['slug_url'])
130-
blog.set_published_at(json.loads(response.text)['data'][0]['attributes']['publishedAt'])
140+
if existing:
141+
blog_id = existing[0]["documentId"]
142+
blog.set_slug_url(existing[0].get("slug_url"))
143+
blog.set_published_at(existing[0].get("published_date"))
131144

132-
url = f"{base_url}/{id}"
133-
create_response = session.put(url, headers=headers, data=json.dumps(blog.get_post_json()))
145+
meta_desc = existing[0].get("meta_desc")
146+
if meta_desc:
147+
blog.meta_desc = meta_desc
134148
else:
135-
# Its a new blog
136-
url = base_url
137-
create_response = session.post(url, headers=headers, data=json.dumps(blog.get_post_json()))
149+
blog.meta_desc = blog.title
150+
151+
print("Updating Blog", blog)
152+
153+
url = f"{base_url}/{blog_id}"
154+
create_response = session.put(
155+
url, headers=headers, data=json.dumps(blog.get_post_json())
156+
)
157+
else:
158+
# New blog
159+
blog.meta_desc = blog.title
160+
blog.set_published_at(
161+
datetime.now(timezone.utc)
162+
.isoformat(timespec="milliseconds")
163+
.replace("+00:00", "Z")
164+
)
165+
print("Adding Blog", blog)
166+
create_response = session.post(
167+
f"{base_url}?status=draft",
168+
headers=headers,
169+
data=json.dumps(blog.get_post_json()),
170+
)
171+
172+
if create_response.status_code not in (200, 201):
173+
print(f"❌ Failed to upload blog: {filepath}", create_response.text)
174+
exit(1)
138175

139-
if not create_response.status_code == 200:
140-
print(f'Error in parsing blog: {filepath}')
141-
print(create_response.text)
142-
exit(1)
143176

144177
def delete_old_blogs():
145-
global existing_filepaths_discovered, BASE_URL
178+
"""Deletes blogs that were not re-uploaded."""
179+
global existing_filepaths_discovered
146180

147-
base_url = urljoin(BASE_URL, 'api/blogs')
181+
base_url = urljoin(BASE_URL, "api/blogs")
148182
session = requests.Session()
149183

150-
for filepath in existing_filepaths_discovered:
151-
if not existing_filepaths_discovered[filepath]['discovered']:
152-
print(f"Deleting filepath: {filepath}")
153-
id = existing_filepaths_discovered[filepath]['id']
154-
if id > 0:
155-
url = f"{base_url}/{id}"
184+
for filepath, info in existing_filepaths_discovered.items():
185+
if not info["discovered"]:
186+
print(f"🗑️ Deleting filepath: {filepath}")
187+
blog_id = info["id"]
188+
if blog_id:
189+
url = f"{base_url}/{blog_id}"
156190
response = session.delete(url, headers=headers)
157-
if response.status_code != 200:
158-
print(f'Error in deleting blog: {filepath}')
159-
print(response.text)
191+
if response.status_code not in (200, 204):
192+
print(f"⚠️ Error deleting {filepath}: {response.text}")
160193

161194

162195
if __name__ == "__main__":
@@ -167,10 +200,11 @@ def delete_old_blogs():
167200

168201
files = find_files_to_upload(items)
169202

170-
print('Uploading blogs')
203+
print("📦 Uploading blogs...")
171204
for file in tqdm(files):
205+
sleep(0.7) # To avoid overwhelming the server
172206
blog = build_blog_object(file)
173207
upload_blog(blog)
174208

175-
print('Deleting blogs')
209+
print("🧹 Cleaning up deleted blogs...")
176210
delete_old_blogs()

0 commit comments

Comments
 (0)