mirror of
https://github.com/alexmickelson/canvasManagement.git
synced 2026-03-25 23:28:33 -06:00
more rate-limit aware posts and deletes
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import { canvasApi, paginatedRequest } from "./canvasServiceUtils";
|
import { canvasApi, paginatedRequest } from "./canvasServiceUtils";
|
||||||
import { CanvasAssignmentGroup } from "@/features/canvas/models/assignments/canvasAssignmentGroup";
|
import { CanvasAssignmentGroup } from "@/features/canvas/models/assignments/canvasAssignmentGroup";
|
||||||
import { LocalAssignmentGroup } from "@/features/local/assignments/models/localAssignmentGroup";
|
import { LocalAssignmentGroup } from "@/features/local/assignments/models/localAssignmentGroup";
|
||||||
import { rateLimitAwareDelete } from "./canvasWebRequestor";
|
import { rateLimitAwareDelete, rateLimitAwarePost } from "./canvasWebRequestUtils";
|
||||||
import { axiosClient } from "@/services/axiosUtils";
|
import { axiosClient } from "@/services/axiosUtils";
|
||||||
|
|
||||||
export const canvasAssignmentGroupService = {
|
export const canvasAssignmentGroupService = {
|
||||||
@@ -26,7 +26,7 @@ export const canvasAssignmentGroupService = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const { data: canvasAssignmentGroup } =
|
const { data: canvasAssignmentGroup } =
|
||||||
await axiosClient.post<CanvasAssignmentGroup>(url, body);
|
await rateLimitAwarePost<CanvasAssignmentGroup>(url, body);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...localAssignmentGroup,
|
...localAssignmentGroup,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { getRubricCriterion } from "./canvasRubricUtils";
|
|||||||
import { LocalCourseSettings } from "@/features/local/course/localCourseSettings";
|
import { LocalCourseSettings } from "@/features/local/course/localCourseSettings";
|
||||||
import { axiosClient } from "@/services/axiosUtils";
|
import { axiosClient } from "@/services/axiosUtils";
|
||||||
import { markdownToHTMLSafe } from "@/services/htmlMarkdownUtils";
|
import { markdownToHTMLSafe } from "@/services/htmlMarkdownUtils";
|
||||||
|
import { rateLimitAwarePost } from "./canvasWebRequestUtils";
|
||||||
|
|
||||||
export const canvasAssignmentService = {
|
export const canvasAssignmentService = {
|
||||||
async getAll(courseId: number): Promise<CanvasAssignment[]> {
|
async getAll(courseId: number): Promise<CanvasAssignment[]> {
|
||||||
@@ -60,7 +61,7 @@ export const canvasAssignmentService = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await axiosClient.post<CanvasAssignment>(url, body);
|
const response = await rateLimitAwarePost<CanvasAssignment>(url, body);
|
||||||
const canvasAssignment = response.data;
|
const canvasAssignment = response.data;
|
||||||
|
|
||||||
await createRubric(canvasCourseId, canvasAssignment.id, localAssignment);
|
await createRubric(canvasCourseId, canvasAssignment.id, localAssignment);
|
||||||
@@ -152,7 +153,7 @@ const createRubric = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
const rubricUrl = `${canvasApi}/courses/${courseId}/rubrics`;
|
const rubricUrl = `${canvasApi}/courses/${courseId}/rubrics`;
|
||||||
const rubricResponse = await axiosClient.post<CanvasRubricCreationResponse>(
|
const rubricResponse = await rateLimitAwarePost<CanvasRubricCreationResponse>(
|
||||||
rubricUrl,
|
rubricUrl,
|
||||||
rubricBody
|
rubricBody
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { CanvasPage } from "@/features/canvas/models/pages/canvasPageModel";
|
|||||||
import { canvasApi, paginatedRequest } from "./canvasServiceUtils";
|
import { canvasApi, paginatedRequest } from "./canvasServiceUtils";
|
||||||
import { CanvasModule } from "@/features/canvas/models/modules/canvasModule";
|
import { CanvasModule } from "@/features/canvas/models/modules/canvasModule";
|
||||||
import { axiosClient } from "@/services/axiosUtils";
|
import { axiosClient } from "@/services/axiosUtils";
|
||||||
|
import { rateLimitAwarePost } from "./canvasWebRequestUtils";
|
||||||
|
|
||||||
export const canvasModuleService = {
|
export const canvasModuleService = {
|
||||||
async updateModuleItem(
|
async updateModuleItem(
|
||||||
@@ -37,7 +38,7 @@ export const canvasModuleService = {
|
|||||||
console.log(`Creating new module item ${title}`);
|
console.log(`Creating new module item ${title}`);
|
||||||
const url = `${canvasApi}/courses/${canvasCourseId}/modules/${canvasModuleId}/items`;
|
const url = `${canvasApi}/courses/${canvasCourseId}/modules/${canvasModuleId}/items`;
|
||||||
const body = { module_item: { title, type, content_id: contentId } };
|
const body = { module_item: { title, type, content_id: contentId } };
|
||||||
await axiosClient.post(url, body);
|
await rateLimitAwarePost(url, body);
|
||||||
},
|
},
|
||||||
|
|
||||||
async createPageModuleItem(
|
async createPageModuleItem(
|
||||||
@@ -51,7 +52,7 @@ export const canvasModuleService = {
|
|||||||
const body = {
|
const body = {
|
||||||
module_item: { title, type: "Page", page_url: canvasPage.url },
|
module_item: { title, type: "Page", page_url: canvasPage.url },
|
||||||
};
|
};
|
||||||
await axiosClient.post<CanvasModuleItem>(url, body);
|
await rateLimitAwarePost<CanvasModuleItem>(url, body);
|
||||||
},
|
},
|
||||||
|
|
||||||
async getCourseModules(canvasCourseId: number) {
|
async getCourseModules(canvasCourseId: number) {
|
||||||
@@ -67,7 +68,7 @@ export const canvasModuleService = {
|
|||||||
name: moduleName,
|
name: moduleName,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const response = await axiosClient.post<CanvasModule>(url, body);
|
const response = await rateLimitAwarePost<CanvasModule>(url, body);
|
||||||
return response.data.id;
|
return response.data.id;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { CanvasPage } from "@/features/canvas/models/pages/canvasPageModel";
|
import { CanvasPage } from "@/features/canvas/models/pages/canvasPageModel";
|
||||||
import { LocalCoursePage } from "@/features/local/pages/localCoursePageModels";
|
import { LocalCoursePage } from "@/features/local/pages/localCoursePageModels";
|
||||||
import { canvasApi, paginatedRequest } from "./canvasServiceUtils";
|
import { canvasApi, paginatedRequest } from "./canvasServiceUtils";
|
||||||
import { rateLimitAwareDelete } from "./canvasWebRequestor";
|
import { rateLimitAwareDelete, rateLimitAwarePost } from "./canvasWebRequestUtils";
|
||||||
import { LocalCourseSettings } from "@/features/local/course/localCourseSettings";
|
import { LocalCourseSettings } from "@/features/local/course/localCourseSettings";
|
||||||
import { axiosClient } from "@/services/axiosUtils";
|
import { axiosClient } from "@/services/axiosUtils";
|
||||||
import { markdownToHTMLSafe } from "@/services/htmlMarkdownUtils";
|
import { markdownToHTMLSafe } from "@/services/htmlMarkdownUtils";
|
||||||
@@ -41,7 +41,7 @@ export const canvasPageService = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const { data: canvasPage } = await axiosClient.post<CanvasPage>(url, body);
|
const { data: canvasPage } = await rateLimitAwarePost<CanvasPage>(url, body);
|
||||||
if (!canvasPage) {
|
if (!canvasPage) {
|
||||||
throw new Error("Created canvas course page was null");
|
throw new Error("Created canvas course page was null");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { LocalCourseSettings } from "@/features/local/course/localCourseSettings
|
|||||||
import { axiosClient } from "@/services/axiosUtils";
|
import { axiosClient } from "@/services/axiosUtils";
|
||||||
import { markdownToHTMLSafe } from "@/services/htmlMarkdownUtils";
|
import { markdownToHTMLSafe } from "@/services/htmlMarkdownUtils";
|
||||||
import { escapeMatchingText } from "@/services/utils/questionHtmlUtils";
|
import { escapeMatchingText } from "@/services/utils/questionHtmlUtils";
|
||||||
|
import { rateLimitAwareDelete, rateLimitAwarePost } from "./canvasWebRequestUtils";
|
||||||
|
|
||||||
export const getAnswers = (
|
export const getAnswers = (
|
||||||
question: LocalQuizQuestion,
|
question: LocalQuizQuestion,
|
||||||
@@ -64,7 +65,7 @@ const createQuestionOnly = async (
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await axiosClient.post<CanvasQuizQuestion>(url, body);
|
const response = await rateLimitAwarePost<CanvasQuizQuestion>(url, body);
|
||||||
const newQuestion = response.data;
|
const newQuestion = response.data;
|
||||||
|
|
||||||
if (!newQuestion) throw new Error("Created question is null");
|
if (!newQuestion) throw new Error("Created question is null");
|
||||||
@@ -86,7 +87,7 @@ const hackFixQuestionOrdering = async (
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const url = `${canvasApi}/courses/${canvasCourseId}/quizzes/${canvasQuizId}/reorder`;
|
const url = `${canvasApi}/courses/${canvasCourseId}/quizzes/${canvasQuizId}/reorder`;
|
||||||
await axiosClient.post(url, { order });
|
await rateLimitAwarePost(url, { order });
|
||||||
};
|
};
|
||||||
|
|
||||||
const verifyQuestionOrder = async (
|
const verifyQuestionOrder = async (
|
||||||
@@ -113,7 +114,7 @@ const verifyQuestionOrder = async (
|
|||||||
// Verify that questions are in the correct order by comparing text content
|
// Verify that questions are in the correct order by comparing text content
|
||||||
// We'll use a simple approach: strip HTML tags and compare the core text content
|
// We'll use a simple approach: strip HTML tags and compare the core text content
|
||||||
const stripHtml = (html: string): string => {
|
const stripHtml = (html: string): string => {
|
||||||
return html.replace(/<[^>]*>/g, '').trim();
|
return html.replace(/<[^>]*>/g, "").trim();
|
||||||
};
|
};
|
||||||
|
|
||||||
for (let i = 0; i < localQuiz.questions.length; i++) {
|
for (let i = 0; i < localQuiz.questions.length; i++) {
|
||||||
@@ -124,8 +125,10 @@ const verifyQuestionOrder = async (
|
|||||||
const canvasQuestionText = stripHtml(canvasQuestion.question_text).trim();
|
const canvasQuestionText = stripHtml(canvasQuestion.question_text).trim();
|
||||||
|
|
||||||
// Check if the question text content matches (allowing for HTML conversion differences)
|
// Check if the question text content matches (allowing for HTML conversion differences)
|
||||||
if (!canvasQuestionText.includes(localQuestionText) &&
|
if (
|
||||||
!localQuestionText.includes(canvasQuestionText)) {
|
!canvasQuestionText.includes(localQuestionText) &&
|
||||||
|
!localQuestionText.includes(canvasQuestionText)
|
||||||
|
) {
|
||||||
console.error(
|
console.error(
|
||||||
`Question order mismatch at position ${i}:`,
|
`Question order mismatch at position ${i}:`,
|
||||||
`Local: "${localQuestionText}"`,
|
`Local: "${localQuestionText}"`,
|
||||||
@@ -135,9 +138,14 @@ const verifyQuestionOrder = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Verify position is correct
|
// Verify position is correct
|
||||||
if (canvasQuestion.position !== undefined && canvasQuestion.position !== i + 1) {
|
if (
|
||||||
|
canvasQuestion.position !== undefined &&
|
||||||
|
canvasQuestion.position !== i + 1
|
||||||
|
) {
|
||||||
console.error(
|
console.error(
|
||||||
`Question position mismatch at index ${i}: Canvas position is ${canvasQuestion.position}, expected ${i + 1}`
|
`Question position mismatch at index ${i}: Canvas position is ${
|
||||||
|
canvasQuestion.position
|
||||||
|
}, expected ${i + 1}`
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -294,7 +302,10 @@ export const canvasQuizService = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const { data: canvasQuiz } = await axiosClient.post<CanvasQuiz>(url, body);
|
const { data: canvasQuiz } = await rateLimitAwarePost<CanvasQuiz>(
|
||||||
|
url,
|
||||||
|
body
|
||||||
|
);
|
||||||
await createQuizQuestions(
|
await createQuizQuestions(
|
||||||
canvasCourseId,
|
canvasCourseId,
|
||||||
canvasQuiz.id,
|
canvasQuiz.id,
|
||||||
@@ -305,6 +316,6 @@ export const canvasQuizService = {
|
|||||||
},
|
},
|
||||||
async delete(canvasCourseId: number, canvasQuizId: number) {
|
async delete(canvasCourseId: number, canvasQuizId: number) {
|
||||||
const url = `${canvasApi}/courses/${canvasCourseId}/quizzes/${canvasQuizId}`;
|
const url = `${canvasApi}/courses/${canvasCourseId}/quizzes/${canvasQuizId}`;
|
||||||
await axiosClient.delete(url);
|
await rateLimitAwareDelete(url);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { axiosClient } from "@/services/axiosUtils";
|
import { axiosClient } from "@/services/axiosUtils";
|
||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse, AxiosRequestConfig } from "axios";
|
||||||
|
|
||||||
const rateLimitRetryCount = 6;
|
const rateLimitRetryCount = 6;
|
||||||
const rateLimitSleepInterval = 1000;
|
const rateLimitSleepInterval = 1000;
|
||||||
@@ -16,21 +16,26 @@ export const isRateLimited = async (
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// const rateLimitAwarePost = async (url: string, body: unknown, retryCount = 0) => {
|
export const rateLimitAwarePost = async <T>(
|
||||||
// const response = await axiosClient.post(url, body);
|
url: string,
|
||||||
|
body: unknown,
|
||||||
|
config?: AxiosRequestConfig,
|
||||||
|
retryCount = 0
|
||||||
|
): Promise<AxiosResponse<T>> => {
|
||||||
|
const response = await axiosClient.post<T>(url, body, config);
|
||||||
|
|
||||||
// if (await isRateLimited(response)) {
|
if (await isRateLimited(response)) {
|
||||||
// if (retryCount < rateLimitRetryCount) {
|
if (retryCount < rateLimitRetryCount) {
|
||||||
// console.info(
|
console.info(
|
||||||
// `Hit rate limit on post, retry count is ${retryCount} / ${rateLimitRetryCount}, retrying`
|
`Hit rate limit on post, retry count is ${retryCount} / ${rateLimitRetryCount}, retrying`
|
||||||
// );
|
);
|
||||||
// await sleep(rateLimitSleepInterval);
|
await sleep(rateLimitSleepInterval);
|
||||||
// return await rateLimitAwarePost(url, body, retryCount + 1);
|
return await rateLimitAwarePost<T>(url, body, config, retryCount + 1);
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
|
|
||||||
// return response;
|
return response;
|
||||||
// };
|
};
|
||||||
|
|
||||||
export const rateLimitAwareDelete = async (
|
export const rateLimitAwareDelete = async (
|
||||||
url: string,
|
url: string,
|
||||||
@@ -4,10 +4,11 @@ import axios from "axios";
|
|||||||
import { canvasApi } from "../canvasServiceUtils";
|
import { canvasApi } from "../canvasServiceUtils";
|
||||||
import { axiosClient } from "@/services/axiosUtils";
|
import { axiosClient } from "@/services/axiosUtils";
|
||||||
import FormData from "form-data";
|
import FormData from "form-data";
|
||||||
|
import { rateLimitAwarePost } from "../canvasWebRequestUtils";
|
||||||
|
|
||||||
export const downloadUrlToTempDirectory = async (
|
export const downloadUrlToTempDirectory = async (
|
||||||
sourceUrl: string
|
sourceUrl: string
|
||||||
): Promise<{fileName: string, success: boolean}> => {
|
): Promise<{ fileName: string; success: boolean }> => {
|
||||||
try {
|
try {
|
||||||
const fileName =
|
const fileName =
|
||||||
path.basename(new URL(sourceUrl).pathname) || `tempfile-${Date.now()}`;
|
path.basename(new URL(sourceUrl).pathname) || `tempfile-${Date.now()}`;
|
||||||
@@ -16,10 +17,10 @@ export const downloadUrlToTempDirectory = async (
|
|||||||
responseType: "arraybuffer",
|
responseType: "arraybuffer",
|
||||||
});
|
});
|
||||||
await fs.writeFile(tempFilePath, response.data);
|
await fs.writeFile(tempFilePath, response.data);
|
||||||
return {fileName: tempFilePath, success: true};
|
return { fileName: tempFilePath, success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("Error downloading or saving the file:", sourceUrl, error);
|
console.log("Error downloading or saving the file:", sourceUrl, error);
|
||||||
return {fileName: sourceUrl, success: false};
|
return { fileName: sourceUrl, success: false };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -45,7 +46,10 @@ export const uploadToCanvasPart1 = async (
|
|||||||
formData.append("name", path.basename(pathToUpload));
|
formData.append("name", path.basename(pathToUpload));
|
||||||
formData.append("size", (await getFileSize(pathToUpload)).toString());
|
formData.append("size", (await getFileSize(pathToUpload)).toString());
|
||||||
|
|
||||||
const response = await axiosClient.post(url, formData);
|
const response = await rateLimitAwarePost<{
|
||||||
|
upload_url: string;
|
||||||
|
upload_params: string;
|
||||||
|
}>(url, formData);
|
||||||
|
|
||||||
const upload_url = response.data.upload_url;
|
const upload_url = response.data.upload_url;
|
||||||
const upload_params = response.data.upload_params;
|
const upload_params = response.data.upload_params;
|
||||||
@@ -77,10 +81,14 @@ export const uploadToCanvasPart2 = async ({
|
|||||||
const fileName = path.basename(pathToUpload);
|
const fileName = path.basename(pathToUpload);
|
||||||
formData.append("file", fileBuffer, fileName);
|
formData.append("file", fileBuffer, fileName);
|
||||||
|
|
||||||
const response = await axiosClient.post(upload_url, formData, {
|
const response = await rateLimitAwarePost<{ url: string }>(
|
||||||
headers: formData.getHeaders(),
|
upload_url,
|
||||||
validateStatus: (status) => status < 500,
|
formData,
|
||||||
});
|
{
|
||||||
|
headers: formData.getHeaders(),
|
||||||
|
validateStatus: (status) => status < 500,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if (response.status === 301) {
|
if (response.status === 301) {
|
||||||
const redirectUrl = response.headers.location;
|
const redirectUrl = response.headers.location;
|
||||||
|
|||||||
Reference in New Issue
Block a user