Skip to content

Commit 24c6858

Browse files
committed
Starting a package manager to automatically update scripts and things.
1 parent a4bb1be commit 24c6858

1 file changed

Lines changed: 235 additions & 0 deletions

File tree

Pckgd/Pckgd.py

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
2+
# Pckgd
3+
# Title: Pckgd
4+
# Description: A module for managing packages and their updates.
5+
# Updates from: github/TenthPres/TouchPointScripts/Pckgd/Pckgd.py
6+
# Version: X
7+
# License: AGPL-3.0
8+
# Author: James Kurtz
9+
10+
# Do not make edits to these files. They will be overwritten during updates.
11+
12+
global model, q, Data
13+
import json
14+
15+
16+
class Pckgd:
17+
def __init__(self, type_id, name, body):
18+
self.filename = name
19+
self.body = body
20+
self.typeId = type_id
21+
self.headers = {}
22+
self.version = None
23+
self.parse_headers()
24+
self.determine_version()
25+
26+
do_not_edit_demarcation = "========="
27+
28+
@staticmethod
29+
def find_installed_packages():
30+
return [Pckgd(p.TypeId, p.Name, p.Body) for p in Pckgd.find_installed_files()]
31+
32+
33+
""" Finds all installed package contents in the system. """
34+
@staticmethod
35+
def find_installed_files():
36+
# noinspection SqlResolve
37+
return q.QuerySql("""
38+
SELECT Name, Body, TypeId, * FROM Content c
39+
WHERE (c.TypeId = 5 AND (
40+
c.Body LIKE '%' + CHAR(10) + '# Pckgd%' OR
41+
c.Body LIKE '# Pckgd%'))
42+
OR (c.Body LIKE '%-- Pckgd%' AND c.TypeId = 4);
43+
""")
44+
45+
""" Parses the headers from the body of the content. """
46+
def parse_headers(self):
47+
lines = self.body.splitlines()
48+
for line in lines:
49+
if (self.typeId == 5 and line.strip().startswith('#')) or \
50+
(self.typeId == 4 and line.strip().startswith('--')):
51+
parts = line[1:].split(':', 1)
52+
if len(parts) == 2:
53+
key = parts[0].strip()
54+
value = parts[1].strip()
55+
56+
# standardize casing of key
57+
key = ' '.join(word.capitalize() for word in key.split())
58+
59+
# Only process headers not seen yet
60+
if key not in self.headers:
61+
self.headers[key] = value
62+
elif Pckgd.do_not_edit_demarcation in line:
63+
break
64+
return
65+
66+
def determine_version(self):
67+
# if version header is set AND it's a hex value or numbered, assume it's right.
68+
if 'Version' in self.headers and self.headers['Version'].strip() != "" and all(c in '0123456789abcdefABCDEF.' for c in self.headers['Version']):
69+
self.version = self.headers['Version']
70+
else:
71+
self.version = Pckgd.calculate_version_hash(self.body)
72+
self.headers['Version'] = self.version
73+
74+
""" Sets or updates a header for a given body. """
75+
@staticmethod
76+
def set_header(body, key, value, type_id):
77+
lines = body.splitlines()
78+
new_lines = []
79+
header_found = False
80+
demarc_found = False
81+
for line in lines:
82+
if (type_id == 5 and line.strip().startswith('#')) or \
83+
(type_id == 4 and line.strip().startswith('--')):
84+
parts = line[1:].split(':', 1)
85+
if len(parts) == 2 and parts[0].strip() == key and not header_found and not demarc_found:
86+
new_lines.append("{} {}: {}".format(('#' if type_id == 5 else '--'), key, value))
87+
header_found = True
88+
else:
89+
new_lines.append(line)
90+
elif Pckgd.do_not_edit_demarcation in line:
91+
demarc_found = True # once we hit the demarcation, stop looking for headers
92+
93+
if not header_found: # Doesn't exist, so needs to be inserted.
94+
insert_index = 0
95+
pckgd_found = False
96+
for i, line in enumerate(new_lines):
97+
if "Pckgd" in line:
98+
insert_index = i + 1 # keep iterating until we get to the Pckgd header
99+
pckgd_found = True
100+
elif not pckgd_found:
101+
insert_index = i + 1
102+
elif (type_id == 5 and line.strip().startswith('#')) or \
103+
(type_id == 4 and line.strip().startswith('--')):
104+
insert_index = i + 1
105+
elif pckgd_found:
106+
break
107+
elif Pckgd.do_not_edit_demarcation in line:
108+
insert_index = -1
109+
110+
if insert_index > -1:
111+
new_lines.insert(insert_index, "{} {}: {}".format(('#' if type_id == 5 else '--'), key, value))
112+
113+
return '\n'.join(new_lines)
114+
115+
""" Calculates a version hash for a given piece of content. This is compared to the version header. """
116+
@staticmethod
117+
def calculate_version_hash(content_body):
118+
import hashlib
119+
hash_object = hashlib.sha256(content_body.strip().encode('utf-8'))
120+
return hash_object.hexdigest()[:8] # return first 8 characters of the hash
121+
122+
123+
def get_header_style(self):
124+
style = ""
125+
if "Header Image" in self.headers:
126+
style += "background-image: url('{}'); background-size: cover; ".format(self.headers['Header Image'])
127+
128+
if 'Header Color' in self.headers:
129+
color = self.headers['Header Color']
130+
else:
131+
color = '#' + Pckgd.calculate_version_hash(self.filename)[:6]
132+
style += "background-color: {}; ".format(color)
133+
return style
134+
135+
def get_update_source(self):
136+
if not 'Updates From' in self.headers:
137+
return None
138+
139+
if self.headers['Updates From'].lower().startswith("github"):
140+
# TODO make this more efficient by caching default branch per repo and make use of other metadata, too.
141+
path = self.headers['Updates From'].split('/', 3)
142+
if len(path) == 4: # some level of validation...
143+
144+
# query github api to get default branch
145+
url = "https://api.github.com/repos/{}/{}".format(path[1], path[2])
146+
147+
response = model.RestGet(url, {"Accept": "application/vnd.github.v3+json"})
148+
response = json.loads(response)
149+
150+
default_branch = "main"
151+
if 'default_branch' in response:
152+
default_branch = response['default_branch']
153+
154+
return "https://raw.githubusercontent.com/{}/{}/refs/heads/{}/{}".format(path[1], path[2], default_branch, path[3])
155+
156+
return None
157+
158+
""" Checks if an update is available for this package. Note that this makes at least one, possibly more, HTTP requests. """
159+
def has_update_available(self):
160+
update_source = self.get_update_source()
161+
if not update_source:
162+
return False
163+
164+
remote_content = model.RestGet(update_source, {})
165+
if remote_content == "404: Not Found": # How Github specifically handles these things.
166+
raise Exception("Update source not found: {}".format(update_source))
167+
168+
remote_pkg = Pckgd(self.typeId, self.filename, remote_content)
169+
return remote_pkg.version != self.version
170+
171+
172+
if model.HttpMethod == "get" and Data.v == "":
173+
model.Header = "Package Manager"
174+
model.Title = "Package Manager"
175+
176+
print("<h2>Installed Packages</h2>\n")
177+
print("<div class=\"section row\">\n")
178+
179+
installed = Pckgd.find_installed_packages()
180+
if (not installed) or (len(installed) == 0):
181+
print("<p>No packages are currently installed.<p />\n")
182+
183+
for p in installed:
184+
print("<div class=\"package col-12 col-sm-6 col-md-4 col-lg-3\">\n")
185+
print("<div class=\"package-header\" style=\"{}\"></div>\n".format(p.get_header_style()))
186+
187+
print("<div class=\"package-body\">\n")
188+
name = p.headers['Name'] if 'Name' in p.headers else p.filename
189+
print("<h3>{}</h3>\n".format(name))
190+
if 'Description' in p.headers:
191+
print("<p>{}</p>\n".format(p.headers['Description']))
192+
193+
try:
194+
if p.has_update_available():
195+
print("<p><strong>Update available!</strong></p>\n")
196+
except Exception as e:
197+
print("<p><strong>Error checking for updates: {}</strong></p>\n".format(str(e)))
198+
199+
pkg_caps = []
200+
if 'Author' in p.headers:
201+
pkg_caps.append("by &nbsp;{}".format(p.headers['Author']))
202+
203+
if 'License' in p.headers:
204+
pkg_caps.append("<span title=\"License\">{}</span>".format(p.headers['License']))
205+
206+
pkg_caps.append("Version:&nbsp;{}".format(p.headers['Version']))
207+
208+
print("<p class=\"package-caption\">{}</p>\n".format(" &bull; ".join(pkg_caps)))
209+
print("</div>\n")
210+
print("</div>\n")
211+
212+
print("</div>\n")
213+
214+
# language=html
215+
print("""
216+
<style>
217+
.package-header {
218+
width: 100%;
219+
padding-top: 25%;
220+
}
221+
.package-body {
222+
padding: 10px;
223+
border-width: 0 1px 1px;
224+
border-style: solid;
225+
border-color: #ccc;
226+
}
227+
div.package-body h3 {
228+
margin-top: 0;
229+
}
230+
p.package-caption {
231+
font-size: 0.75em;
232+
opacity: 0.7;
233+
}
234+
</style>
235+
""")

0 commit comments

Comments
 (0)