Skip to content

Commit ca83437

Browse files
committed
Pckgd headers
1 parent 92f035a commit ca83437

5 files changed

Lines changed: 219 additions & 36 deletions

File tree

AssignShepherd/AssignShepherd.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
# Pckgd
2+
# Title: Assign Shepherd
3+
# Description: A module for assigning "shepherds" to families.
4+
# Updates from: github/TenthPres/TouchPointScripts/AssignShepherd/AssignShepherd.py
5+
# Author: James at Tenth
6+
17
# noinspection PyUnresolvedReferences
28
import clr
39

BackgroundChecks/BackgroundChecks-Status.sql

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
-- Pckgd
2+
-- Updates from: github/TenthPres/TouchPointScripts/BackgroundChecks/BackgroundChecks-Status.sql
3+
14
DECLARE @delinquentDate DATE;
25
SET @delinquentDate = (SELECT CAST(GETDATE() - 10 AS DATE));
36

BackgroundChecks/BackgroundChecks.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,20 @@
11
# noinspection PyUnresolvedReferences
2+
3+
# Pckgd
4+
# Title: PA Background Checks
5+
# Description: Make Pennsylvania background checks just a little better.
6+
# Updates from: github/TenthPres/TouchPointScripts/BackgroundChecks/BackgroundChecks.py
7+
# Author: James at Tenth
8+
# Requires: BackgroundChecks-Status.sql
9+
210
from System import DateTime, String, Convert
311

412
global model, Data
513

614
mainQuery = model.SqlContent('BackgroundChecks-Status')
15+
16+
17+
718
userPerson = model.GetPerson(model.UserPeopleId)
819
subjectPid = userPerson.PeopleId
920
model.Header = "Background Checks"

Calendar/IntegrationPelican.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
1+
# Pckgd
2+
# Title: Pelican HVAC Integration
3+
# Description: Room Reservations controlling Pelican HVAC thermostats.
4+
# Updates from: github/TenthPres/TouchPointScripts/Calendar/IntegrationPelican.py
5+
# Author: James at Tenth
6+
7+
18
global model, Data, q
29

310
from urllib import urlencode
411
import xml.etree.ElementTree as ET
512
import json
613

7-
8-
914
site = ""
1015
username = ""
1116
password = ""

Pckgd/Pckgd.py

Lines changed: 192 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
1-
21
# Pckgd
3-
# Title: Pckgd
2+
# Title: Pckgd, A Package Manager
43
# Description: A module for managing packages and their updates.
54
# Updates from: github/TenthPres/TouchPointScripts/Pckgd/Pckgd.py
65
# Version: 0.0.2
76
# License: AGPL-3.0
8-
# Author: James Kurtz
7+
# Author: James at Tenth
98

10-
# Do not make edits to these files. They will be overwritten during updates.
9+
# Do not make edits to this file. They will be overwritten during updates.
1110

1211
global model, q, Data
1312
import json
13+
import time
1414

1515

1616
class Pckgd:
@@ -20,10 +20,16 @@ def __init__(self, type_id, name, body):
2020
self.typeId = type_id
2121
self.headers = {}
2222
self.version = None
23+
self._has_update_available = None
2324
self.parse_headers()
2425
self.determine_version()
26+
self.add_dependencies()
27+
28+
def __del__(self):
29+
Pckgd._save_meta_if_dirty()
2530

2631
do_not_edit_demarcation = "========="
32+
dependents = {}
2733

2834
@staticmethod
2935
def find_installed_packages():
@@ -48,7 +54,8 @@ def parse_headers(self):
4854
for line in lines:
4955
if (self.typeId == 5 and line.strip().startswith('#')) or \
5056
(self.typeId == 4 and line.strip().startswith('--')):
51-
parts = line[1:].split(':', 1)
57+
prefix_chars = 1 if self.typeId == 5 else 2
58+
parts = line[prefix_chars:].split(':', 1)
5259
if len(parts) == 2:
5360
key = parts[0].strip()
5461
value = parts[1].strip()
@@ -65,28 +72,110 @@ def parse_headers(self):
6572

6673
def get_action_buttons(self):
6774
buttons = []
68-
if 'Updates From' in self.headers:
69-
buttons.append({
70-
"label": "View Update Source",
71-
"url": self.get_update_source(),
72-
"class": "btn-primary"
73-
})
7475

75-
return buttons
76+
if self.typeId == 5:
77+
buttons.append("""
78+
<a href="/PyScript/{}" class="btn btn-default">Run</a>
79+
""".format(self.filename))
80+
elif self.typeId == 4:
81+
buttons.append("""
82+
<a href="/RunScript/{}" class="btn btn-default">Run</a>
83+
""".format(self.filename))
84+
85+
link = self.get_repo_link()
86+
if link:
87+
buttons.append("""
88+
<a href="{}" class="btn btn-default" target="_blank" title="View on GitHub"><i class="fa fa-github"></i></a>
89+
""".format(link))
90+
91+
if self.has_update_available():
92+
buttons.append("""
93+
<a href="PyScript/Pckgd?v=update&pkg={}" class="btn btn-primary">Update</a>
94+
""".format(self.filename))
95+
96+
return "\n".join(buttons)
7697

7798
def determine_version(self):
7899
# if version header is set AND it's a hex value or numbered, assume it's right.
79100
if 'Version' in self.headers and self.headers['Version'].strip() != "" and all(c in '0123456789abcdefABCDEF.' for c in self.headers['Version']):
80101
self.version = self.headers['Version']
81102
else:
82103
# find relevant part of body to hash
83-
b = self.body.split("Pckgd", 1)[1]
104+
b = self.body
105+
if "Pckgd" in self.body:
106+
b = b.split("Pckgd", 1)[1]
84107
if Pckgd.do_not_edit_demarcation in b:
85108
b = b.split(Pckgd.do_not_edit_demarcation, 1)[1]
86109

110+
b = b.strip().replace('\r\n', '\n').replace('\r', '\n')
111+
87112
self.version = Pckgd.calculate_version_hash(b)
88113
self.headers['Version'] = self.version
89114

115+
116+
def add_dependencies(self):
117+
if 'Requires' in self.headers:
118+
dependencies = [d.strip() for d in self.headers['Requires'].split(',')]
119+
for dep in dependencies:
120+
if dep not in Pckgd.dependents:
121+
Pckgd.dependents[dep] = []
122+
Pckgd.dependents[dep].append(self.filename_with_extension())
123+
124+
125+
def dependents_list(self):
126+
if self.filename_with_extension() in Pckgd.dependents:
127+
return Pckgd.dependents[self.filename_with_extension()]
128+
return []
129+
130+
def dependencies_list(self):
131+
if 'Requires' in self.headers:
132+
return [d.strip() for d in self.headers['Requires'].split(',')]
133+
return []
134+
135+
def has_dependents(self):
136+
deps = self.dependents_list()
137+
return len(deps)
138+
139+
def has_dependencies(self):
140+
return len(self.dependencies_list())
141+
142+
def dependencies_with_updates_available(self):
143+
updates = []
144+
for dep in self.dependencies_list():
145+
dep_pkg = None
146+
for p in Pckgd.find_installed_packages():
147+
if p.filename_with_extension() == dep:
148+
dep_pkg = p
149+
break
150+
if dep_pkg and dep_pkg.has_update_available():
151+
updates.append((dep, dep_pkg.has_update_available()))
152+
return updates
153+
154+
def get_repo_link(self):
155+
if not 'Updates From' in self.headers:
156+
return None
157+
158+
if self.headers['Updates From'].lower().startswith("github"):
159+
path = self.headers['Updates From'].split('/', 3)
160+
if len(path) == 4: # some level of validation...
161+
github_meta = Pckgd._get_github_repo_metadata(path[1] + "/" + path[2])
162+
163+
default_branch = "main"
164+
if 'default_branch' in github_meta:
165+
default_branch = github_meta['default_branch']
166+
167+
return "https://github.com/{}/{}/blob/{}/{}".format(path[1], path[2], default_branch, path[3])
168+
return None
169+
170+
171+
def filename_with_extension(self):
172+
if self.typeId == 5:
173+
return self.filename + ".py"
174+
elif self.typeId == 4:
175+
return self.filename + ".sql"
176+
else:
177+
return self.filename
178+
90179
""" Sets or updates a header for a given body. """
91180
@staticmethod
92181
def set_header(body, key, value, type_id):
@@ -153,36 +242,77 @@ def get_update_source(self):
153242
return None
154243

155244
if self.headers['Updates From'].lower().startswith("github"):
156-
# TODO make this more efficient by caching default branch per repo and make use of other metadata, too.
157245
path = self.headers['Updates From'].split('/', 3)
158246
if len(path) == 4: # some level of validation...
159-
160-
# query github api to get default branch
161-
url = "https://api.github.com/repos/{}/{}".format(path[1], path[2])
162-
163-
response = model.RestGet(url, {"Accept": "application/vnd.github.v3+json"})
164-
response = json.loads(response)
247+
github_meta = Pckgd._get_github_repo_metadata(path[1] + "/" + path[2])
165248

166249
default_branch = "main"
167-
if 'default_branch' in response:
168-
default_branch = response['default_branch']
250+
if 'default_branch' in github_meta:
251+
default_branch = github_meta['default_branch']
169252

170253
return "https://raw.githubusercontent.com/{}/{}/refs/heads/{}/{}".format(path[1], path[2], default_branch, path[3])
171254

172255
return None
173256

174257
""" Checks if an update is available for this package. Note that this makes at least one, possibly more, HTTP requests. """
175258
def has_update_available(self):
176-
update_source = self.get_update_source()
177-
if not update_source:
178-
return False
259+
if self._has_update_available is None:
260+
261+
update_source = self.get_update_source()
262+
if not update_source:
263+
self._has_update_available = False
264+
return self._has_update_available
265+
266+
remote_content = model.RestGet(update_source, {})
267+
if remote_content == "404: Not Found": # How GitHub specifically handles these things.
268+
self._has_update_available = False
269+
raise Exception("Update source not found: {}".format(update_source))
270+
271+
remote_pkg = Pckgd(self.typeId, self.filename, remote_content)
272+
if remote_pkg.version != self.version:
273+
self._has_update_available = remote_pkg.version
274+
else:
275+
self._has_update_available = False
276+
return self._has_update_available
277+
278+
_saved_meta = None
279+
280+
@staticmethod
281+
def _get_saved_meta():
282+
if Pckgd._saved_meta is None:
283+
saved_meta = model.TextContent("PckgdCache.json")
284+
if saved_meta.strip() == "":
285+
Pckgd._saved_meta = {}
286+
else:
287+
Pckgd._saved_meta = json.loads(saved_meta)
288+
return Pckgd._saved_meta
289+
290+
@staticmethod
291+
def _get_github_repo_metadata(repo_path):
292+
meta = Pckgd._get_saved_meta()
293+
294+
if not "github_repo_meta" in meta:
295+
meta["github_repo_meta"] = {}
179296

180-
remote_content = model.RestGet(update_source, {})
181-
if remote_content == "404: Not Found": # How Github specifically handles these things.
182-
raise Exception("Update source not found: {}".format(update_source))
297+
if not repo_path in meta["github_repo_meta"]:
298+
meta["github_repo_meta"][repo_path] = {}
183299

184-
remote_pkg = Pckgd(self.typeId, self.filename, remote_content)
185-
return remote_pkg.version != self.version
300+
if '_expires' not in meta["github_repo_meta"][repo_path] or meta["github_repo_meta"][repo_path]['_expires'] < time.time():
301+
# query GitHub api to get default branch and other such stuff
302+
url = "https://api.github.com/repos/{}".format(repo_path)
303+
response = model.RestGet(url, {"Accept": "application/vnd.github.v3+json"})
304+
meta["github_repo_meta"][repo_path]['data'] = json.loads(response)
305+
meta["github_repo_meta"][repo_path]['_expires'] = time.time() + 86400 # cache for 1 day
306+
meta['_dirty'] = True
307+
308+
return meta["github_repo_meta"][repo_path]['data']
309+
310+
@staticmethod
311+
def _save_meta_if_dirty():
312+
meta = Pckgd._get_saved_meta()
313+
if '_dirty' in meta and meta['_dirty']:
314+
del meta['_dirty']
315+
model.WriteContentText("PckgdCache.json", json.dumps(meta, indent=2))
186316

187317

188318
if model.HttpMethod == "get" and Data.v == "":
@@ -197,31 +327,55 @@ def has_update_available(self):
197327
print("<p>No packages are currently installed.<p />\n")
198328

199329
for p in installed:
330+
if p.has_dependents():
331+
continue # skip packages that have dependents; they will be shown with their parent package.
332+
200333
print("<div class=\"package col-12 col-sm-6 col-md-4 col-lg-3\">\n")
201334
print("<div class=\"package-header\" style=\"{}\"></div>\n".format(p.get_header_style()))
202335

203336
print("<div class=\"package-body\">\n")
204-
name = p.headers['Name'] if 'Name' in p.headers else p.filename
337+
name = p.headers['Title'] if 'Title' in p.headers else p.filename
205338
print("<h3>{}</h3>\n".format(name))
206339
if 'Description' in p.headers:
207340
print("<p>{}</p>\n".format(p.headers['Description']))
208341

342+
update = False
209343
try:
210-
if p.has_update_available():
211-
print("<p><strong>Update available!</strong></p>\n")
344+
update = p.has_update_available()
212345
except Exception as e:
213346
print("<p><strong>Error checking for updates: {}</strong></p>\n".format(str(e)))
214347

348+
update_dep = False
349+
try:
350+
update_dep = p.dependencies_with_updates_available()
351+
except Exception as e:
352+
print("<p><strong>Error checking for dependency updates: {}</strong></p>\n".format(str(e)))
353+
354+
if update or update_dep:
355+
print("<p><strong style=\"color:#600\">Update available.</strong></p>\n")
356+
215357
pkg_caps = []
216358
if 'Author' in p.headers:
217-
pkg_caps.append("by &nbsp;{}".format(p.headers['Author']))
359+
pkg_caps.append("by&nbsp;{}".format(p.headers['Author']))
218360

219361
if 'License' in p.headers:
220362
pkg_caps.append("<span title=\"License\">{}</span>".format(p.headers['License']))
221363

222364
pkg_caps.append("Version:&nbsp;{}".format(p.headers['Version']))
223365

366+
if len(update_dep) > 0:
367+
pkg_caps.append("<span style=\"color:#600\" title=\"{0} files are ready to be updated.\">&#x21bb;&nbsp;{0} Files</span>".format(1 * (not not update) + len(update_dep)))
368+
369+
elif update:
370+
pkg_caps.append("<span style=\"color:#600\" title=\"New Version Available\">&#x21bb;&nbsp;{}</span>".format(update))
371+
372+
if p.has_dependencies() or p.has_dependents():
373+
pkg_caps.append("<span title=\"{0} Dependents: files that depend on this file\">{0}</span>:<span title=\"{1} Dependencies: files on which this file depends\">{1}</span>".format(p.has_dependents(), p.has_dependencies()))
374+
224375
print("<p class=\"package-caption\">{}</p>\n".format(" &bull; ".join(pkg_caps)))
376+
377+
print(p.get_action_buttons())
378+
225379
print("</div>\n")
226380
print("</div>\n")
227381

@@ -239,6 +393,10 @@ def has_update_available(self):
239393
border-width: 0 1px 1px;
240394
border-style: solid;
241395
border-color: #ccc;
396+
height: 14em;
397+
margin-bottom: 2em;
398+
overflow-y: auto;
399+
overflow-x: hidden;
242400
}
243401
div.package-body h3 {
244402
margin-top: 0;

0 commit comments

Comments
 (0)