Coverage for src/lanraragi_api/api/database.py: 93%

30 statements  

« prev     ^ index     » next       coverage.py v7.16.0, created at 2026-09-22 23:20 +0000

1from os.path import isfile 

2 

3from requests import Response 

4 

5from lanraragi_api.api.base import ( 

6 BaseAPICall, 

7) 

8from lanraragi_api.entity.base import OperationResponse 

9from lanraragi_api.entity.database import DatabaseBackup, TagStatistic 

10from lanraragi_api.entity.minion import MinionJobResponse 

11 

12 

13class DatabaseAPI(BaseAPICall): 

14 """Database management APIs. 

15 

16 Shared request and error behavior is documented on ``BaseAPICall``. 

17 """ 

18 

19 def get_tag_statistics( 

20 self, 

21 min_weight: int = 1, 

22 hide_excluded_namespaces: bool | None = None, 

23 ) -> list[TagStatistic]: 

24 """Get tags from the database, with a value symbolizing their prevalence. 

25 

26 Args: 

27 min_weight: Only get tags whose weight is at least the given 

28 minimum. Defaults to 1, which gets all tags. 

29 hide_excluded_namespaces: Set to True to exclude tags whose 

30 namespace is configured in the server settings. Defaults to 

31 None, which returns all tags. 

32 

33 Returns: 

34 list[TagStatistic]: One entry per tag, with its namespace, text and 

35 weight. 

36 

37 Raises: 

38 APIHttpError: Any status code other than 200. 

39 APIResponseDecodeError: If the response body is not valid JSON, or 

40 if it is not a list of ``TagStatistic`` objects. 

41 """ 

42 hide_excluded = None 

43 if hide_excluded_namespaces is not None: 

44 hide_excluded = "true" if hide_excluded_namespaces else "false" 

45 

46 return self.request_model_list( 

47 "GET", 

48 "/api/database/stats", 

49 TagStatistic, 

50 params={ 

51 "minweight": min_weight, 

52 "hide_excluded_namespaces": hide_excluded, 

53 }, 

54 ) 

55 

56 def clean_database(self) -> OperationResponse: 

57 """Clean the Database. 

58 

59 Entries for files that are no longer on the filesystem are hidden and 

60 then removed. They are only unlinked at first, so they do not appear in 

61 the UI; a subsequent run of this cleanup deletes the unlinked entries. 

62 

63 Returns: 

64 OperationResponse: Result of the cleanup, including the amount of 

65 ``deleted`` and ``unlinked`` entries reported by the server. 

66 

67 Raises: 

68 APIResponseDecodeError: If the response body is not valid JSON, or 

69 does not match ``OperationResponse``. 

70 APIOperationError: If the operation failed and raising is enabled. 

71 """ 

72 return self.request_operation("POST", "/api/database/clean") 

73 

74 def drop_database(self) -> OperationResponse: 

75 """Delete the entire database, including user preferences. 

76 

77 This is a rather dangerous endpoint: invoking it might lock you out of 

78 the server as a client. 

79 

80 Returns: 

81 OperationResponse: Result of the database drop. 

82 

83 Raises: 

84 APIResponseDecodeError: If the response body is not valid JSON, or 

85 does not match ``OperationResponse``. 

86 APIOperationError: If the operation failed and raising is enabled. 

87 """ 

88 return self.request_operation("POST", "/api/database/drop") 

89 

90 def get_backup(self) -> DatabaseBackup: 

91 """Scan the entire database and return a backup in JSON form. 

92 

93 Consider using ``queue_backup`` if your database is large, as this 

94 basic GET endpoint might time out if it takes too long to generate the 

95 backup. 

96 

97 This backup can be reimported manually through the Backup and Restore 

98 feature. 

99 

100 Returns: 

101 DatabaseBackup: Archive, category and tankoubon metadata of the 

102 entire database. 

103 

104 Raises: 

105 APIHttpError: Any status code other than 200. 

106 APIResponseDecodeError: If the response body is not valid JSON, or 

107 does not match ``DatabaseBackup``. 

108 """ 

109 return self.request_model("GET", "/api/database/backup", DatabaseBackup) 

110 

111 def queue_backup(self) -> MinionJobResponse: 

112 """Queue a Minion job to generate a backup JSON file. 

113 

114 Use the returned job ID to check progress, then download the file once 

115 the job is complete through ``download_backup``. 

116 

117 Returns: 

118 MinionJobResponse: Enqueued job, whose ``job`` field holds the ID 

119 of the Minion job. 

120 

121 Raises: 

122 APIResponseDecodeError: If the response body is not valid JSON, or 

123 does not match ``MinionJobResponse``. 

124 APIOperationError: If the operation failed and raising is enabled. 

125 """ 

126 return self.request_operation( 

127 "POST", "/api/database/backup", model=MinionJobResponse 

128 ) 

129 

130 def download_backup(self, jobid: int, format: str | None = None) -> Response: 

131 """Download the backup JSON file generated by a completed backup job. 

132 

133 Args: 

134 jobid: ID of the completed backup job. 

135 format: Format of the returned backup. ``json`` returns the backup 

136 as a JSON response, while ``file`` returns it as a file. 

137 Defaults to None, which the server treats as ``file``. 

138 

139 Returns: 

140 Response: Raw response of the server, holding the backup file or 

141 JSON payload. 

142 

143 Raises: 

144 APIHttpError: 400 if the job is not found or not completed yet, or 

145 any other non-2xx status code. 

146 """ 

147 return self.request( 

148 "GET", 

149 f"/api/database/backup/{jobid}", 

150 params={"format": format}, 

151 ) 

152 

153 def queue_restore(self, file_path: str) -> MinionJobResponse: 

154 """Queue a Minion job to restore from a backup JSON file. 

155 

156 Use the returned job ID to check progress. 

157 

158 Args: 

159 file_path: Path to the backup JSON file to restore. Backslashes are 

160 normalized to forward slashes before the file is looked up. 

161 

162 Returns: 

163 MinionJobResponse: Enqueued job, whose ``job`` field holds the ID 

164 of the Minion job. 

165 

166 Raises: 

167 FileNotFoundError: If ``file_path`` does not point to an existing 

168 file. 

169 APIResponseDecodeError: If the response body is not valid JSON, or 

170 does not match ``MinionJobResponse``. 

171 APIOperationError: If the operation failed and raising is enabled. 

172 

173 Note: 

174 A 400 response, returned for an invalid request such as a malformed 

175 backup file, is returned in the operation result, with ``success`` 

176 set to 0, instead of raising. 

177 """ 

178 file_path = file_path.replace("\\", "/") 

179 

180 if not isfile(file_path): 

181 raise FileNotFoundError(f"File {file_path} not found") 

182 

183 with open(file_path, "rb") as backup_file: 

184 return self.request_operation( 

185 "POST", 

186 "/api/database/restore", 

187 model=MinionJobResponse, 

188 files={ 

189 "file": ( 

190 file_path.split("/")[-1], 

191 backup_file, 

192 "application/octet-stream", 

193 ) 

194 }, 

195 ) 

196 

197 def clear_all_new_flags(self) -> OperationResponse: 

198 """Clear the "New!" flag on all archives. 

199 

200 Returns: 

201 OperationResponse: Result of the flag clearing operation. 

202 

203 Raises: 

204 APIResponseDecodeError: If the response body is not valid JSON, or 

205 does not match ``OperationResponse``. 

206 APIOperationError: If the operation failed and raising is enabled. 

207 """ 

208 return self.request_operation("DELETE", "/api/database/isnew")