From be6d61e567903b03da8aacb35b057a1b0f0cab2d Mon Sep 17 00:00:00 2001 From: Phil Budne Date: Thu, 12 Mar 2026 18:26:08 -0400 Subject: [PATCH 1/3] Sketch (it's only a model) of a DirectoryManagementApi class --- mediacloud/api.py | 6 ++++- mediacloud/mgmt.py | 63 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 mediacloud/mgmt.py diff --git a/mediacloud/api.py b/mediacloud/api.py index abb42de..d9610fa 100644 --- a/mediacloud/api.py +++ b/mediacloud/api.py @@ -60,9 +60,13 @@ def _query(self, endpoint: str, params: Optional[Dict] = None, method: str = 'GE r = self._session.get(endpoint_url, params=params, timeout=self.TIMEOUT_SECS) elif method == 'POST': r = self._session.post(endpoint_url, json=params, timeout=self.TIMEOUT_SECS) + elif method == "DELETE": + return self._session.delete(endpoint_url, params=params, timeout=self.TIMEOUT_SECS) + elif method == "PATCH": + return self._session.patch(endpoint_url, json=params, timeout=self.TIMEOUT_SECS) else: raise RuntimeError(f"Unsupported method of '{method}'") - if r.status_code != 200: + if r.status_code // 100 != 2: raise mediacloud.error.APIResponseError(r, params, r.json()) return r.json() diff --git a/mediacloud/mgmt.py b/mediacloud/mgmt.py new file mode 100644 index 0000000..e088b2e --- /dev/null +++ b/mediacloud/mgmt.py @@ -0,0 +1,63 @@ +""" +Directory Management API + +Nod to Monty Python +It's not Camelot, only a model... + +update/patch methods not yet tested!!! +""" + +from mediacloud.api import BaseApi + + +class DirectoryManagementApi(BaseApi): + + ################ CollectionsViewSet + + def collection_create(self, params: dict) -> dict: + # XXX take discrete args, create dict? + # XXX require name! + # possible others: notes + # bools: public, featured, managed, monitored + return self._query(f'sources/collections/', params, "POST") + + def collection_update(self, collection_id: int, params: dict) -> dict: + # XXX take discrete args, create dict? + return self._query(f'sources/collections/{collection_id}/', params, "PATCH") + + # for testing/cleanup: + def _collection_delete(self, collection_id: int) -> dict: + return self._query(f'sources/collections/{collection_id}/', None, "DELETE") + + ################ SourcesViewSet + + def source_create(self, params: dict) -> dict: + # XXX take discrete args, create dict? + # XXX require name, label, homepage? + # back end defaults: platform + # optional: url_search_string, notes, media_type, pub_{state,country}, primary_language + return self._query('sources/sources/', params, "POST") + + def source_update(self, source_id: int, params: dict) -> dict: + # XXX take discrete args, create dict? + return self._query(f'sources/sources/{source_id}/', params, "PATCH") + + # for testing/cleanup: + def source_delete(self, source_id: int) -> dict: + return self._query(f'sources/sources/{source_id}/', None, "DELETE") + + ################ SourcesCollectionsViewSet + + def source_collection_list(self, source_id: int) -> dict: + # XXX belongs in DirectoryApi?? + # XXX take either source_id or collection_id!!! + return self._query(f'sources/sources-collections/{source_id}/', None, "GET") + + # mcweb sourcesCollectionsApi.js calls this createSourceCollectionAssociation + def source_collection_create(self, source_id: int, collection_id: int) -> dict: + params = { 'source_id': source_id, 'collection_id': collection_id } + return self._query('sources/sources-collections/', params, "POST") + + # mcweb sourcesCollectionsApi.js calls this deleteSourceCollectionAssociation + def source_collection_delete(self, source_id: int, collection_id: int) -> dict: + return self._query(f'sources/sources-collections/{source_id}/?collection_id={collection_id}', "DELETE") From 46c949247041018acea05478d474636537f3757d Mon Sep 17 00:00:00 2001 From: Phil Budne Date: Fri, 13 Mar 2026 00:49:05 -0400 Subject: [PATCH 2/3] fancied up create/update parameter handling --- mediacloud/mgmt.py | 102 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 77 insertions(+), 25 deletions(-) diff --git a/mediacloud/mgmt.py b/mediacloud/mgmt.py index e088b2e..77083b0 100644 --- a/mediacloud/mgmt.py +++ b/mediacloud/mgmt.py @@ -5,41 +5,85 @@ It's not Camelot, only a model... update/patch methods not yet tested!!! + +ALL arguments are keyword only, for safety! """ +from typing import TypeAlias from mediacloud.api import BaseApi +_EMPTY = object() -class DirectoryManagementApi(BaseApi): +_Params: TypeAlias = dict - ################ CollectionsViewSet - def collection_create(self, params: dict) -> dict: - # XXX take discrete args, create dict? - # XXX require name! - # possible others: notes - # bools: public, featured, managed, monitored - return self._query(f'sources/collections/', params, "POST") +class DirectoryManagementApi(BaseApi): # XXX maybe extend DirectoryApi??? + """ + Class for Directory Management + """ + + def _params(self, what: str, kws: dict, params: list[str]) -> _Params: + """ + helper for _{collection,source}_params helpers + """ + ret: _Params = {} + kwcopy = kws.copy() + for p in params: + v = kwcopy.pop(p, _EMPTY) + if v is not _EMPTY: + ret[p] = v + if kwcopy: + extra = ",".join(kwcopy.keys()) + raise ValueError(f"Unknown {what} params {extra}") + return ret + + ################ CollectionsViewSet - def collection_update(self, collection_id: int, params: dict) -> dict: - # XXX take discrete args, create dict? + def _collection_params(self, kws: dict) -> _Params: + """ + helper for collection_{create,update} + """ + return self._params("collection", kws, + ['name', 'note', 'public', 'featured', 'managed', 'monitored']) + + def collection_create(self, **kwargs) -> dict: + params = self._collection_params(kwargs) + if 'name' not in params or not params['name']: + raise ValueError("collection_create must have 'name'") + return self._query('sources/collections/', params, "POST") + + def collection_update(self, *, collection_id: int, **kwargs) -> dict: + params = self._collection_params(**kwargs) + if not params: + raise ValueError("no parameters for collection_update?") return self._query(f'sources/collections/{collection_id}/', params, "PATCH") # for testing/cleanup: - def _collection_delete(self, collection_id: int) -> dict: + def collection_delete(self, collection_id: int) -> dict: return self._query(f'sources/collections/{collection_id}/', None, "DELETE") ################ SourcesViewSet - def source_create(self, params: dict) -> dict: - # XXX take discrete args, create dict? - # XXX require name, label, homepage? - # back end defaults: platform - # optional: url_search_string, notes, media_type, pub_{state,country}, primary_language + def _source_params(self, kws: dict) -> _Params: + """ + helper for source_{create,update} + """ + return self._params("source", kws, + ['name', 'label', 'homepage', 'platform', + 'url_search_string', 'notes', 'media_type', + 'pub_state', 'pub_country', 'primary_language']) + + def source_create(self, **kwargs) -> dict: + params = self._source_params(kwargs) + for p in ['name', 'homepage']: + if p not in params: + raise ValueError(f"source_create requires '{p}'") return self._query('sources/sources/', params, "POST") - def source_update(self, source_id: int, params: dict) -> dict: - # XXX take discrete args, create dict? + def source_update(self, *, source_id: int, **kwargs) -> dict: + params = self._source_params(kwargs) + if not params: + raise ValueError("no parameters for source_update?") return self._query(f'sources/sources/{source_id}/', params, "PATCH") # for testing/cleanup: @@ -48,16 +92,24 @@ def source_delete(self, source_id: int) -> dict: ################ SourcesCollectionsViewSet - def source_collection_list(self, source_id: int) -> dict: - # XXX belongs in DirectoryApi?? - # XXX take either source_id or collection_id!!! - return self._query(f'sources/sources-collections/{source_id}/', None, "GET") + def source_collection_list(self, *, + source_id: int | None = None, + collection_id: int | None = None) -> dict: + if source_id and collection_id: + raise ValueError("source_collection_list got both source_id and collection_id") + if source_id: + endpoint = f'sources/sources-collections/{source_id}/' + else: + endpoint = f'sources/sources-collections/{collection_id}/?collection=1' # XXX untested + return self._query(endpoint, None, "GET") # mcweb sourcesCollectionsApi.js calls this createSourceCollectionAssociation - def source_collection_create(self, source_id: int, collection_id: int) -> dict: - params = { 'source_id': source_id, 'collection_id': collection_id } + def source_collection_create(self, *, source_id: int, collection_id: int) -> dict: + params = {'source_id': source_id, 'collection_id': collection_id} return self._query('sources/sources-collections/', params, "POST") # mcweb sourcesCollectionsApi.js calls this deleteSourceCollectionAssociation - def source_collection_delete(self, source_id: int, collection_id: int) -> dict: + # XXX endpoint seems to take collection=bool query parameter?? + # (if not set to true, expects collection_id parameter??) + def source_collection_delete(self, *, source_id: int, collection_id: int) -> dict: return self._query(f'sources/sources-collections/{source_id}/?collection_id={collection_id}', "DELETE") From 2ba52a4cf7721d8afc170a3a014a0c0cb0bec079 Mon Sep 17 00:00:00 2001 From: Phil Budne Date: Fri, 13 Mar 2026 16:18:12 -0400 Subject: [PATCH 3/3] more mgmt.py twerking add collection_copy, collection_source_list fix collection_update, _collection_params --- mediacloud/api.py | 2 +- mediacloud/mgmt.py | 40 ++++++++++++++++++++++++++++------------ 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/mediacloud/api.py b/mediacloud/api.py index d9610fa..a8443d9 100644 --- a/mediacloud/api.py +++ b/mediacloud/api.py @@ -66,7 +66,7 @@ def _query(self, endpoint: str, params: Optional[Dict] = None, method: str = 'GE return self._session.patch(endpoint_url, json=params, timeout=self.TIMEOUT_SECS) else: raise RuntimeError(f"Unsupported method of '{method}'") - if r.status_code // 100 != 2: + if r.status_code // 100 != 2: # create operations return 201 raise mediacloud.error.APIResponseError(r, params, r.json()) return r.json() diff --git a/mediacloud/mgmt.py b/mediacloud/mgmt.py index 77083b0..76eb308 100644 --- a/mediacloud/mgmt.py +++ b/mediacloud/mgmt.py @@ -44,7 +44,7 @@ def _collection_params(self, kws: dict) -> _Params: helper for collection_{create,update} """ return self._params("collection", kws, - ['name', 'note', 'public', 'featured', 'managed', 'monitored']) + ['name', 'notes', 'public', 'featured', 'managed', 'monitored']) def collection_create(self, **kwargs) -> dict: params = self._collection_params(kwargs) @@ -52,8 +52,15 @@ def collection_create(self, **kwargs) -> dict: raise ValueError("collection_create must have 'name'") return self._query('sources/collections/', params, "POST") + def collection_copy(self, *, collection_id: int, name: str) -> dict: + # name defaults to "original name (copy)", but be fussy: + if not name: + raise ValueError("collection_copy must have 'name'") + params = {'collection_id': collection_id, 'name': name} + return self._query('sources/collections/copy-collection/', params, "POST") + def collection_update(self, *, collection_id: int, **kwargs) -> dict: - params = self._collection_params(**kwargs) + params = self._collection_params(kwargs) if not params: raise ValueError("no parameters for collection_update?") return self._query(f'sources/collections/{collection_id}/', params, "PATCH") @@ -62,6 +69,15 @@ def collection_update(self, *, collection_id: int, **kwargs) -> dict: def collection_delete(self, collection_id: int) -> dict: return self._query(f'sources/collections/{collection_id}/', None, "DELETE") + def collection_source_list(self, *, collection_id: int) -> list[dict]: + """ + returns list of source objects for a collection + """ + # XXX should do pagination!!!!!!! + ret = self._query(f'sources/sources/?collection_id={collection_id}', None, "GET") + # returns {'count': n, 'next': ..., 'previous': ..., 'results': [{...}, ...]} + return ret['results'] + ################ SourcesViewSet def _source_params(self, kws: dict) -> _Params: @@ -84,6 +100,10 @@ def source_update(self, *, source_id: int, **kwargs) -> dict: params = self._source_params(kwargs) if not params: raise ValueError("no parameters for source_update?") + params['id'] = source_id + # currently causes Internal Server Error and returned with body: + # {"detail":"{'homepage': [ErrorDetail(string='This field is required.', code='required')]}"} + # which isn't what I expect from "PATCH"!!! return self._query(f'sources/sources/{source_id}/', params, "PATCH") # for testing/cleanup: @@ -92,16 +112,12 @@ def source_delete(self, source_id: int) -> dict: ################ SourcesCollectionsViewSet - def source_collection_list(self, *, - source_id: int | None = None, - collection_id: int | None = None) -> dict: - if source_id and collection_id: - raise ValueError("source_collection_list got both source_id and collection_id") - if source_id: - endpoint = f'sources/sources-collections/{source_id}/' - else: - endpoint = f'sources/sources-collections/{collection_id}/?collection=1' # XXX untested - return self._query(endpoint, None, "GET") + def source_collection_list(self, *, source_id: int) -> list[dict]: + """ + returns list of collection objects for a source + """ + ret = self._query(f'sources/sources-collections/{source_id}/', None, "GET") + return ret['collections'] # mcweb sourcesCollectionsApi.js calls this createSourceCollectionAssociation def source_collection_create(self, *, source_id: int, collection_id: int) -> dict: