cornellGrading 2.26.0__tar.gz → 2.27.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. {cornellgrading-2.26.0/cornellGrading.egg-info → cornellgrading-2.27.0}/PKG-INFO +1 -1
  2. {cornellgrading-2.26.0 → cornellgrading-2.27.0}/cornellGrading/__init__.py +2 -1
  3. cornellgrading-2.27.0/cornellGrading/simplelists.py +242 -0
  4. {cornellgrading-2.26.0 → cornellgrading-2.27.0/cornellGrading.egg-info}/PKG-INFO +1 -1
  5. {cornellgrading-2.26.0 → cornellgrading-2.27.0}/cornellGrading.egg-info/SOURCES.txt +1 -0
  6. {cornellgrading-2.26.0 → cornellgrading-2.27.0}/LICENSE +0 -0
  7. {cornellgrading-2.26.0 → cornellgrading-2.27.0}/MANIFEST.in +0 -0
  8. {cornellgrading-2.26.0 → cornellgrading-2.27.0}/README.md +0 -0
  9. {cornellgrading-2.26.0 → cornellgrading-2.27.0}/cornellGrading/cornellGrading.py +0 -0
  10. {cornellgrading-2.26.0 → cornellgrading-2.27.0}/cornellGrading/cornellInterface.py +0 -0
  11. {cornellgrading-2.26.0 → cornellgrading-2.27.0}/cornellGrading/cornellQualtrics.py +0 -0
  12. {cornellgrading-2.26.0 → cornellgrading-2.27.0}/cornellGrading/dueDatesFromCSV.py +0 -0
  13. {cornellgrading-2.26.0 → cornellgrading-2.27.0}/cornellGrading/pandocHTMLParser.py +0 -0
  14. {cornellgrading-2.26.0 → cornellgrading-2.27.0}/cornellGrading/quizQuestionBank.py +0 -0
  15. {cornellgrading-2.26.0 → cornellgrading-2.27.0}/cornellGrading/uploadPollEvScores.py +0 -0
  16. {cornellgrading-2.26.0 → cornellgrading-2.27.0}/cornellGrading/upload_MC_questions.py +0 -0
  17. {cornellgrading-2.26.0 → cornellgrading-2.27.0}/cornellGrading/utils.py +0 -0
  18. {cornellgrading-2.26.0 → cornellgrading-2.27.0}/cornellGrading.egg-info/dependency_links.txt +0 -0
  19. {cornellgrading-2.26.0 → cornellgrading-2.27.0}/cornellGrading.egg-info/entry_points.txt +0 -0
  20. {cornellgrading-2.26.0 → cornellgrading-2.27.0}/cornellGrading.egg-info/requires.txt +0 -0
  21. {cornellgrading-2.26.0 → cornellgrading-2.27.0}/cornellGrading.egg-info/top_level.txt +0 -0
  22. {cornellgrading-2.26.0 → cornellgrading-2.27.0}/pyproject.toml +0 -0
  23. {cornellgrading-2.26.0 → cornellgrading-2.27.0}/scripts/canvastex.py +0 -0
  24. {cornellgrading-2.26.0 → cornellgrading-2.27.0}/scripts/matlabgrader.py +0 -0
  25. {cornellgrading-2.26.0 → cornellgrading-2.27.0}/scripts/setupSelfGrading.py +0 -0
  26. {cornellgrading-2.26.0 → cornellgrading-2.27.0}/setup.cfg +0 -0
  27. {cornellgrading-2.26.0 → cornellgrading-2.27.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cornellGrading
3
- Version: 2.26.0
3
+ Version: 2.27.0
4
4
  Summary: Routines for interacting with Cornell installations of Canvas and Qualtrics
5
5
  Home-page: https://github.com/dsavransky/grading
6
6
  Author: Dmitry Savransky
@@ -1,4 +1,5 @@
1
1
  name = "cornellGrading"
2
- __version__ = "2.26.0"
2
+ __version__ = "2.27.0"
3
3
  from .cornellGrading import cornellGrading
4
4
  from .cornellQualtrics import cornellQualtrics
5
+ from .simplelists import simplelists
@@ -0,0 +1,242 @@
1
+ """Wrapper class for interacting with the SimpleLists v2 REST API.
2
+
3
+ Provides authentication against per-list API keys stored in a local JSON
4
+ credentials file, plus basic contact operations (retrieve, create, delete)
5
+ against SimpleLists mailing lists"""
6
+
7
+ import json
8
+ import os
9
+ from pathlib import Path
10
+ from typing import Any, Dict, List, Optional
11
+
12
+ import requests
13
+
14
+
15
+ class simplelists:
16
+ """Class for io methods for the SimpleLists v2 API."""
17
+
18
+ def __init__(self, server: str = "https://lists.cornell.edu/api/2/") -> None:
19
+ """Load per-list API keys and validate them against the live API.
20
+
21
+ Args:
22
+ server (str):
23
+ Base URL of the SimpleLists API, including trailing slash
24
+ and version path. Defaults to
25
+ 'https://lists.cornell.edu/api/2/'.
26
+ """
27
+
28
+ self.baseurl = server
29
+
30
+ cdir = "simplelists"
31
+
32
+ if os.name == "nt":
33
+ config_dir = Path(os.environ["APPDATA"], cdir)
34
+ else:
35
+ config_dir = Path(Path.home(), ".config", cdir)
36
+
37
+ credfile = config_dir / "credentials.json"
38
+
39
+ assert credfile.exists(), f"Cannot locate credentials file at {credfile}."
40
+ assert os.access(credfile, os.R_OK), (
41
+ f"Credentials file at {credfile} exists but is not readable."
42
+ )
43
+
44
+ self.keys: Dict[str, str] = json.loads(credfile.read_text())
45
+
46
+ for account, apikey in self.keys.items():
47
+ server_response = requests.get(
48
+ f"{self.baseurl}contacts/", params={"limit": 1}, auth=(apikey, "")
49
+ )
50
+ assert server_response.status_code == 200, (
51
+ f"Could not authenticate SimpleLists API key for '{account}'."
52
+ )
53
+
54
+ def _get_api_key(self, list_name: str) -> str:
55
+ """Look up the API key associated with a mailing list name.
56
+
57
+ Args:
58
+ list_name (str):
59
+ Name of the list (e.g. 'MAEFIELDFACULTY-L'), matching the
60
+ list-name portion of a '<list_name>-account' key in the
61
+ loaded credentials file.
62
+
63
+ Returns:
64
+ str:
65
+ API key for the requested list.
66
+ """
67
+
68
+ account = f"{list_name}-account"
69
+ assert account in self.keys, (
70
+ f"No API key found for list '{list_name}' "
71
+ f"(expected key '{account}' in credentials.json)."
72
+ )
73
+
74
+ return self.keys[account]
75
+
76
+ def get_contacts(self, list_name: str, limit: int = 1000) -> List[Dict[str, Any]]:
77
+ """Retrieve all contacts belonging to a mailing list.
78
+
79
+ Args:
80
+ list_name (str):
81
+ Name of the list to query (e.g. 'MAEFIELDFACULTY-L').
82
+ limit (int):
83
+ Maximum number of records to request per page (SimpleLists
84
+ API allows up to 1000). Defaults to 1000.
85
+
86
+ Returns:
87
+ list:
88
+ Raw contact objects (as returned by the API, one dict per
89
+ contact) for every contact on the list.
90
+ """
91
+
92
+ apikey = self._get_api_key(list_name)
93
+ url = f"{self.baseurl}contacts/"
94
+
95
+ records: List[Dict[str, Any]] = []
96
+ page = 1
97
+ while True:
98
+ server_response = requests.get(
99
+ url,
100
+ params={"list": list_name, "page": page, "limit": limit},
101
+ auth=(apikey, ""),
102
+ )
103
+ assert server_response.status_code == 200, (
104
+ f"Could not retrieve contacts for list '{list_name}' (page {page})."
105
+ )
106
+ payload = server_response.json()
107
+ records.extend(payload["data"])
108
+
109
+ if not payload["data"] or page * limit >= payload["count"]:
110
+ break
111
+ page += 1
112
+
113
+ return records
114
+
115
+ def create_contact(
116
+ self,
117
+ list_name: str,
118
+ email: str,
119
+ firstname: Optional[str] = None,
120
+ surname: Optional[str] = None,
121
+ notes: Optional[str] = None,
122
+ ) -> Dict[str, Any]:
123
+ """Create a new contact and add them to a mailing list.
124
+
125
+ Args:
126
+ list_name (str):
127
+ Name of the list to add the new contact to.
128
+ email (str):
129
+ Primary email address for the new contact.
130
+ firstname (str, optional):
131
+ Contact's first name. Defaults to None (omitted).
132
+ surname (str, optional):
133
+ Contact's surname. Defaults to None (omitted).
134
+ notes (str, optional):
135
+ Free-text notes for the contact. Defaults to None (omitted).
136
+
137
+ Returns:
138
+ dict:
139
+ Parsed JSON response describing the created contact.
140
+ """
141
+
142
+ apikey = self._get_api_key(list_name)
143
+ url = f"{self.baseurl}contacts/"
144
+
145
+ payload: Dict[str, Any] = {
146
+ "emails": email,
147
+ "lists": list_name,
148
+ }
149
+ if firstname is not None:
150
+ payload["firstname"] = firstname
151
+ if surname is not None:
152
+ payload["surname"] = surname
153
+ if notes is not None:
154
+ payload["notes"] = notes
155
+
156
+ server_response = requests.post(url, data=payload, auth=(apikey, ""))
157
+ assert server_response.status_code == 200, (
158
+ f"Could not create contact '{email}' on list '{list_name}'."
159
+ )
160
+
161
+ return server_response.json()
162
+
163
+ def update_contact(
164
+ self,
165
+ list_name: str,
166
+ contact_id: int,
167
+ firstname: Optional[str] = None,
168
+ surname: Optional[str] = None,
169
+ notes: Optional[str] = None,
170
+ email: Optional[str] = None,
171
+ ) -> Dict[str, Any]:
172
+ """Update an existing contact.
173
+
174
+ Args:
175
+ list_name (str):
176
+ Name of the list the API key belongs to (used only to
177
+ resolve the API key; SimpleLists contact ids are global,
178
+ not scoped to a single list).
179
+ contact_id (int):
180
+ SimpleLists internal id of the contact to update.
181
+ firstname (str, optional):
182
+ New first name. Defaults to None (left unchanged).
183
+ surname (str, optional):
184
+ New surname. Defaults to None (left unchanged).
185
+ notes (str, optional):
186
+ New free-text notes. Defaults to None (left unchanged).
187
+ email (str, optional):
188
+ New list of email addresses. Defaults to None (left
189
+ unchanged).
190
+
191
+ Returns:
192
+ dict:
193
+ Parsed JSON response describing the updated contact.
194
+ """
195
+
196
+ apikey = self._get_api_key(list_name)
197
+ url = f"{self.baseurl}contacts/{contact_id}/"
198
+
199
+ payload: Dict[str, Any] = {}
200
+ if firstname is not None:
201
+ payload["firstname"] = firstname
202
+ if surname is not None:
203
+ payload["surname"] = surname
204
+ if notes is not None:
205
+ payload["notes"] = notes
206
+ if email is not None:
207
+ payload["emails"] = email
208
+
209
+ assert payload, "At least one contact property must be provided to update."
210
+
211
+ server_response = requests.put(url, data=payload, auth=(apikey, ""))
212
+ assert server_response.status_code == 200, (
213
+ f"Could not update contact id {contact_id} (list '{list_name}')."
214
+ )
215
+
216
+ return server_response.json()
217
+
218
+ def delete_contact(self, list_name: str, contact_id: int) -> bool:
219
+ """Delete a contact from SimpleLists.
220
+
221
+ Args:
222
+ list_name (str):
223
+ Name of the list the API key belongs to (used only to
224
+ resolve the API key; SimpleLists contact ids are global,
225
+ not scoped to a single list).
226
+ contact_id (int):
227
+ SimpleLists internal id of the contact to delete.
228
+
229
+ Returns:
230
+ bool:
231
+ True if the API reported successful deletion.
232
+ """
233
+
234
+ apikey = self._get_api_key(list_name)
235
+ url = f"{self.baseurl}contacts/{contact_id}/"
236
+
237
+ server_response = requests.delete(url, auth=(apikey, ""))
238
+ assert server_response.status_code == 200, (
239
+ f"Could not delete contact id {contact_id} (list '{list_name}')."
240
+ )
241
+
242
+ return server_response.json().get("success", False)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cornellGrading
3
- Version: 2.26.0
3
+ Version: 2.27.0
4
4
  Summary: Routines for interacting with Cornell installations of Canvas and Qualtrics
5
5
  Home-page: https://github.com/dsavransky/grading
6
6
  Author: Dmitry Savransky
@@ -11,6 +11,7 @@ cornellGrading/cornellQualtrics.py
11
11
  cornellGrading/dueDatesFromCSV.py
12
12
  cornellGrading/pandocHTMLParser.py
13
13
  cornellGrading/quizQuestionBank.py
14
+ cornellGrading/simplelists.py
14
15
  cornellGrading/uploadPollEvScores.py
15
16
  cornellGrading/upload_MC_questions.py
16
17
  cornellGrading/utils.py
File without changes