From f10330cfb1ec706b45ddaf6b01190388497d0759 Mon Sep 17 00:00:00 2001 From: palewire Date: Thu, 19 Oct 2023 04:57:20 -0400 Subject: [PATCH 1/6] Refactor to use a shared post method --- datawrapper/__main__.py | 284 +++++++++++++++++----------------------- 1 file changed, 122 insertions(+), 162 deletions(-) diff --git a/datawrapper/__main__.py b/datawrapper/__main__.py index f773ea0..0732aaa 100644 --- a/datawrapper/__main__.py +++ b/datawrapper/__main__.py @@ -19,7 +19,7 @@ import os from io import StringIO from pathlib import Path -from typing import Any, Iterable +from typing import Any import IPython import pandas as pd @@ -68,7 +68,7 @@ def __init__(self, access_token=_ACCESS_TOKEN): self._access_token = access_token self._auth_header = {"Authorization": f"Bearer {access_token}"} - def get(self, url: str, params: dict | None = None, timeout: int = 15): + def get(self, url: str, params: dict | None = None, timeout: int = 15) -> Any: """Make a GET request to the Datawrapper API. Parameters @@ -82,8 +82,8 @@ def get(self, url: str, params: dict | None = None, timeout: int = 15): Returns ------- - dict - A dictionary containing the response from the API. + Any + An object containing the response from the API. """ # Set headers headers = self._auth_header @@ -113,7 +113,59 @@ def get(self, url: str, params: dict | None = None, timeout: int = 15): logger.error(f"Request failed with status code {response.status_code}.") raise FailedRequest(response) - def account_info(self) -> dict[str, Any]: + def post( + self, + url: str, + data: dict | None = None, + timeout: int = 15, + extra_headers: dict | None = None, + ) -> dict: + """Make a POST request to the Datawrapper API. + + Parameters + ---------- + url : str + The URL to request. + data : dict + A dictionary of data to pass to the request, by default None + timeout : int, optional + The timeout for the request in seconds, by default 15 + extra_headers : dict, optional + A dictionary of extra headers to pass to the request, by default None + + Returns + ------- + dict + A dictionary containing the response from the API. + """ + # Set headers + headers = self._auth_header + headers["accept"] = "*/*" + + # Add extra headers if provided + if extra_headers: + headers.update(extra_headers) + + # Set kwargs to post + kwargs = {"headers": headers, "timeout": timeout} + + # Convert data to json + if data: + kwargs["data"] = json.dumps(data) + + # Make the request + response = r.post(url, **kwargs) + + # Check if the request was successful + if response.ok: + # Return the data as json + return response.json() + # If not, raise an exception + else: + logger.error(f"Request failed with status code {response.status_code}.") + raise FailedRequest(response) + + def account_info(self) -> dict: """A deprecated method for calling get_my_account.""" # Issue a deprecation warning logger.warning( @@ -124,7 +176,7 @@ def account_info(self) -> dict[str, Any]: # Use the newer method return self.get_my_account() - def get_my_account(self) -> dict[str, Any]: + def get_my_account(self) -> dict: """Access your account information. Returns @@ -169,7 +221,7 @@ def update_my_account( _header["accept"] = "*/*" _header["content-type"] = "application/json" - _query: dict[str, Any] = {} + _query: dict = {} if name: _query["name"] = name if email: @@ -223,7 +275,7 @@ def update_my_settings( _header["accept"] = "*/*" _header["content-type"] = "application/json" - _query: dict[str, Any] = {} + _query: dict = {} if active_team: _query["activeTeam"] = active_team @@ -250,7 +302,7 @@ def get_my_recently_edited_charts( limit: str | int = 100, offset: str | int = 0, min_last_edit_step: str | int = 0, - ) -> dict[str, Any]: + ) -> dict: """Get a list of your recently edited charts. Parameters @@ -269,7 +321,7 @@ def get_my_recently_edited_charts( dict A dictionary with the list of charts and metadata about the selection. """ - _query: dict[str, Any] = {} + _query: dict = {} if limit: _query["limit"] = limit if offset: @@ -287,7 +339,7 @@ def get_my_recently_published_charts( limit: str | int = 100, offset: str | int = 0, min_last_edit_step: str | int = 0, - ) -> dict[str, Any]: + ) -> dict: """Get a list of your recently published charts. Parameters @@ -306,7 +358,7 @@ def get_my_recently_published_charts( dict A dictionary with the list of charts and metadata about the selection. """ - _query: dict[str, Any] = {} + _query: dict = {} if limit: _query["limit"] = limit if offset: @@ -321,7 +373,7 @@ def get_my_recently_published_charts( def get_themes( self, limit: str | int = 100, offset: str | int = 0, deleted: bool = False - ) -> dict[str, Any]: + ) -> dict: """Get a list of themes in your Datawrapper account. Parameters @@ -383,7 +435,7 @@ def add_data(self, chart_id: str, data: pd.DataFrame | str) -> r.Response: data=_data.encode("utf-8"), ) - def refresh_data(self, chart_id: str) -> r.Response: + def refresh_data(self, chart_id: str) -> dict: """Fetch configured external data and add it to the chart. Parameters @@ -393,16 +445,10 @@ def refresh_data(self, chart_id: str) -> r.Response: Returns ------- - requests.Response - A requests.Response + dict + A dictionary containing the chart's information. """ - _header = self._auth_header - _header["accept"] = "*/*" - - return r.post( - url=f"{self._CHARTS_URL}/{chart_id}/data/refresh", - headers=_header, - ) + return self.post(f"{self._CHARTS_URL}/{chart_id}/data/refresh") def create_chart( self, @@ -411,8 +457,8 @@ def create_chart( data: pd.DataFrame | str | None = None, folder_id: str = "", organization_id: str = "", - metadata: dict[Any, Any] | None = None, - ) -> dict[Any, Any] | None | Any: + metadata: dict | None = None, + ) -> dict | None | Any: """Creates a new Datawrapper chart, table or map. You can pass a pandas DataFrame as a `data` argument to upload data. @@ -443,12 +489,8 @@ def create_chart( dict A dictionary containing the created chart's information. """ - - _header = self._auth_header - _header["content-type"] = "application/json" - + # Set chart properties _data = {"title": title, "type": chart_type} - if folder_id: _data["folderId"] = folder_id if organization_id: @@ -456,33 +498,14 @@ def create_chart( if metadata: _data["metadata"] = metadata # type: ignore - new_chart_response = r.post( - url=self._CHARTS_URL, headers=_header, data=json.dumps(_data) + # Create chart + chart_info = self.post( + self._CHARTS_URL, + data=_data, + extra_headers={"content-type": "application/json"}, ) - if ( - chart_type == "d3-maps-choropleth" - or chart_type == "d3-maps-symbols" - or chart_type == "locator-map" - ): - logger.debug( - "\nNOTE: Maps need a valid basemap, set in properties -> visualize" - ) - logger.debug( - ( - "Full list of valid maps can be retrieved with\n\n", - "curl --request GET --url https://api.datawrapper.de/plugin/basemap\n", - ) - ) - - if new_chart_response.status_code <= 201: - chart_info = new_chart_response.json() - logger.debug(f"New chart {chart_info['type']} created!") - else: - msg = f"Chart could not be created, check your authorization credentials (access token){', and that the folder_id is valid (i.e exists, and your account has access to it)' if folder_id else ''}" - logger.error(msg) - raise Exception(msg) - + # Add data if provided if data is not None: self.add_data(chart_id=chart_info["id"], data=data) @@ -557,7 +580,7 @@ def update_description( logger.error(msg) raise Exception(msg) - def publish_chart(self, chart_id: str, display: bool = True) -> Any | None: + def publish_chart(self, chart_id: str, display: bool = True) -> dict | HTML: """Publishes a chart, table or map. Parameters @@ -566,30 +589,23 @@ def publish_chart(self, chart_id: str, display: bool = True) -> Any | None: ID of chart, table or map. display : bool, optional Display the published chart as output in notebook cell, by default True - """ - publish_chart_response = r.post( - url=f"{self._PUBLISH_URL}/{chart_id}/publish", - headers=self._auth_header, - ) - if publish_chart_response.status_code <= 201: - publish_chart_info = publish_chart_response.json() - logger.debug(f"Chart published at {publish_chart_info['url']}") - if display: - iframe_code = publish_chart_info["data"]["metadata"]["publish"][ - "embed-codes" - ]["embed-method-iframe"] - return HTML(iframe_code) - else: - return None + Returns + ------- + dict | HTML + Either a dictionary containing the published chart's information or an HTML + object displaying the chart. + """ + chart_info = self.post(f"{self._PUBLISH_URL}/{chart_id}/publish") + if display: + iframe_code = chart_info["data"]["metadata"]["publish"]["embed-codes"][ + "embed-method-iframe" + ] + return HTML(iframe_code) else: - msg = "Chart couldn't be published at this time." - logger.error(msg) - raise Exception(msg) + return chart_info - def chart_properties( - self, chart_id: str - ) -> dict[Any, Any] | None | Any | Iterable[Any]: + def chart_properties(self, chart_id: str) -> dict: """Retrieve information of a specific chart, table or map. Parameters @@ -619,7 +635,7 @@ def chart_data(self, chart_id: str): """ return self.get(self._CHARTS_URL + f"/{chart_id}/data") - def update_metadata(self, chart_id: str, properties: dict[Any, Any]) -> Any | None: + def update_metadata(self, chart_id: str, properties: dict) -> Any | None: """Update a chart, table, or map's metadata. Example: https://developer.datawrapper.de/docs/creating-a-chart-new#edit-colors @@ -856,7 +872,7 @@ def export_chart( logger.error(msg) raise Exception(msg) - def get_basemaps(self) -> list[dict[str, Any]]: + def get_basemaps(self) -> list[dict]: """Get a list of the available basemaps. Returns @@ -866,7 +882,7 @@ def get_basemaps(self) -> list[dict[str, Any]]: """ return self.get(self._BASEMAPS_URL) - def get_basemap(self, basemap_id: str, wgs84: bool = False) -> dict[str, Any]: + def get_basemap(self, basemap_id: str, wgs84: bool = False) -> dict: """Get the metdata of the requested basemap. Parameters @@ -886,7 +902,7 @@ def get_basemap(self, basemap_id: str, wgs84: bool = False) -> dict[str, Any]: params={"wgs84": wgs84}, ) - def get_basemap_key(self, basemap_id: str, basemap_key: str) -> dict[str, Any]: + def get_basemap_key(self, basemap_id: str, basemap_key: str) -> dict: """Get the list of available values for a basemap's key. Parameters @@ -903,7 +919,7 @@ def get_basemap_key(self, basemap_id: str, basemap_key: str) -> dict[str, Any]: """ return self.get(f"{self._BASEMAPS_URL}/{basemap_id}/{basemap_key}") - def get_folders(self) -> dict[Any, Any] | None | Any: + def get_folders(self) -> dict | None | Any: """Get a list of folders in your Datawrapper account. Returns @@ -914,7 +930,7 @@ def get_folders(self) -> dict[Any, Any] | None | Any: """ return self.get(self._FOLDERS_URL) - def get_folder(self, folder_id: str | int) -> dict[Any, Any]: + def get_folder(self, folder_id: str | int) -> dict: """Get an existing folder. Parameters @@ -934,7 +950,7 @@ def create_folder( name: str, parent_id: str | int | None = None, team_id: str | int | None = None, - ) -> dict[Any, Any]: + ) -> dict: """Create a new folder. Parameters @@ -952,32 +968,18 @@ def create_folder( dict A dictionary containing the folder's information. """ - _header = self._auth_header - _header["accept"] = "*/*" - - _query: dict[str, Any] = {"name": name} + _query: dict = {"name": name} if parent_id: _query["parentId"] = parent_id if team_id: _query["teamId"] = team_id - response = r.post( - url=self._FOLDERS_URL, - headers=_header, - data=json.dumps(_query), + return self.post( + self._FOLDERS_URL, + data=_query, + extra_headers={"content-type": "application/json"}, ) - if response.ok: - folder_info = response.json() - logger.debug( - f"Folder {folder_info['name']} created with id {folder_info['id']}" - ) - return folder_info - else: - msg = "Folder could not be created." - logger.error(msg) - raise Exception(msg) - def update_folder( self, folder_id: str | int, @@ -985,7 +987,7 @@ def update_folder( parent_id: str | int | None = None, team_id: str | int | None = None, user_id: str | int | None = None, - ) -> dict[Any, Any]: + ) -> dict: """Update an existing folder. Parameters @@ -1009,7 +1011,7 @@ def update_folder( _header = self._auth_header _header["accept"] = "*/*" - _query: dict[str, Any] = {} + _query: dict = {} if name: _query["name"] = name if parent_id: @@ -1095,7 +1097,7 @@ def move_chart(self, chart_id: str, folder_id: str) -> Any | None: logger.error(msg) raise Exception(msg) - def copy_chart(self, chart_id: str) -> dict[Any, Any]: + def copy_chart(self, chart_id: str) -> dict: """Copy one of your charts, tables, or maps and create a new editable copy. Parameters @@ -1108,22 +1110,9 @@ def copy_chart(self, chart_id: str) -> dict[Any, Any]: dict A dictionary containing the information of the chart, table, or map. """ - _header = self._auth_header - _header["accept"] = "*/*" - - url = f"{self._CHARTS_URL}/{chart_id}/copy" - response = r.post(url=url, headers=_header) - - if response.ok: - copy_id = response.json() - logger.debug(f"Chart {chart_id} copied to {copy_id['id']}") - return copy_id - else: - msg = "Chart could not be copied at the moment." - logger.error(msg) - raise Exception(msg) + return self.post(f"{self._CHARTS_URL}/{chart_id}/copy") - def fork_chart(self, chart_id: str) -> dict[Any, Any]: + def fork_chart(self, chart_id: str) -> dict: """Fork a chart, table, or map and create an editable copy. Parameters @@ -1136,23 +1125,7 @@ def fork_chart(self, chart_id: str) -> dict[Any, Any]: dict A dictionary containing the information of the chart, table, or map. """ - _header = self._auth_header - _header["accept"] = "*/*" - - url = f"{self._CHARTS_URL}/{chart_id}/fork" - response = r.post(url=url, headers=_header) - - if response.ok: - fork = response.json() - logger.debug(f"Chart {chart_id} copied to {fork['id']}") - return fork - else: - msg = ( - "Chart could not be forked. If it's a chart you created, ", - "you should trying copying it instead.", - ) - logger.error(msg) - raise Exception(msg) + return self.post(f"{self._CHARTS_URL}/{chart_id}/fork") def delete_chart(self, chart_id: str) -> r.Response.content: # type: ignore """Deletes a specified chart, table or map. @@ -1216,7 +1189,7 @@ def get_charts( list List of charts. """ - _query: dict[str, Any] = {} + _query: dict = {} if user_id: _query["userId"] = user_id if published: @@ -1243,7 +1216,7 @@ def get_teams( order_by: str = "name", limit: int = 100, offset: int = 0, - ) -> dict[str, Any]: + ) -> dict: """Get a list of teams in your Datawrapper account. Parameters @@ -1264,7 +1237,7 @@ def get_teams( dict A dictionary containing the teams in your Datawrapper account. """ - _query: dict[str, Any] = {} + _query: dict = {} if search: _query["search"] = search if order: @@ -1282,7 +1255,7 @@ def create_team( self, name: str, default_theme: str | None = None, - ) -> dict[str, Any]: + ) -> dict: """Create a new team. Parameters @@ -1297,30 +1270,17 @@ def create_team( dict A dictionary containing the team's information. """ - _header = self._auth_header - _header["accept"] = "*/*" - _header["content-type"] = "application/json" - - _query: dict[str, Any] = {"name": name} + _query: dict = {"name": name} if default_theme: _query["defaultTheme"] = default_theme - response = r.post( - url=self._TEAMS_URL, - headers=_header, - data=json.dumps(_query), + return self.post( + self._TEAMS_URL, + data=_query, + extra_headers={"content-type": "application/json"}, ) - if response.ok: - team_info = response.json() - logger.debug(f"Team {team_info['name']} created with id {team_info['id']}") - return team_info - else: - msg = "Team could not be created." - logger.error(msg) - raise Exception(msg) - - def get_team(self, team_id: str) -> dict[str, Any]: + def get_team(self, team_id: str) -> dict: """Get an existing team. Parameters @@ -1340,7 +1300,7 @@ def update_team( team_id: str, name: str | None = None, default_theme: str | None = None, - ) -> dict[str, Any]: + ) -> dict: """Update an existing team. Parameters From 47ed86b3044e205b6e545db4c69a9ed4268bd5ed Mon Sep 17 00:00:00 2001 From: palewire Date: Thu, 19 Oct 2023 05:02:41 -0400 Subject: [PATCH 2/6] mypy fixes --- datawrapper/__main__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/datawrapper/__main__.py b/datawrapper/__main__.py index 0732aaa..797ba41 100644 --- a/datawrapper/__main__.py +++ b/datawrapper/__main__.py @@ -458,7 +458,7 @@ def create_chart( folder_id: str = "", organization_id: str = "", metadata: dict | None = None, - ) -> dict | None | Any: + ) -> dict: """Creates a new Datawrapper chart, table or map. You can pass a pandas DataFrame as a `data` argument to upload data. @@ -919,7 +919,7 @@ def get_basemap_key(self, basemap_id: str, basemap_key: str) -> dict: """ return self.get(f"{self._BASEMAPS_URL}/{basemap_id}/{basemap_key}") - def get_folders(self) -> dict | None | Any: + def get_folders(self) -> dict: """Get a list of folders in your Datawrapper account. Returns From 0663c611f1aa38cabe128581f7ba01a364c9272f Mon Sep 17 00:00:00 2001 From: palewire Date: Thu, 19 Oct 2023 16:49:15 -0400 Subject: [PATCH 3/6] Refactor to use a common patch method --- datawrapper/__main__.py | 210 +++++++++++++++------------------------- 1 file changed, 80 insertions(+), 130 deletions(-) diff --git a/datawrapper/__main__.py b/datawrapper/__main__.py index 797ba41..fe28be3 100644 --- a/datawrapper/__main__.py +++ b/datawrapper/__main__.py @@ -110,7 +110,57 @@ def get(self, url: str, params: dict | None = None, timeout: int = 15) -> Any: return response.text # If not, raise an exception else: - logger.error(f"Request failed with status code {response.status_code}.") + logger.error(f"Get request failed with status code {response.status_code}.") + raise FailedRequest(response) + + def patch( + self, + url: str, + data: dict | None = None, + timeout: int = 15, + extra_headers: dict | None = None, + ) -> dict: + """Make a PATCH request to the Datawrapper API. + + Parameters + ---------- + url : str + The URL to request. + data : dict + A dictionary of data to pass to the request, by default None + timeout : int, optional + The timeout for the request in seconds, by default 15 + extra_headers : dict, optional + A dictionary of extra headers to pass to the request, by default None + """ + # Set headers + headers = self._auth_header + headers["accept"] = "*/*" + headers["content-type"] = "application/json" + + # Add extra headers if provided + if extra_headers: + headers.update(extra_headers) + + # Set kwargs to post + kwargs = {"headers": headers, "timeout": timeout} + + # Convert data to json + if data: + kwargs["data"] = json.dumps(data) + + # Make the request + response = r.patch(url, **kwargs) + + # Check if the request was successful + if response.ok: + # Return the data as json + return response.json() + # If not, raise an exception + else: + logger.error( + f"Patch request failed with status code {response.status_code}." + ) raise FailedRequest(response) def post( @@ -162,7 +212,9 @@ def post( return response.json() # If not, raise an exception else: - logger.error(f"Request failed with status code {response.status_code}.") + logger.error( + f"Post request failed with status code {response.status_code}." + ) raise FailedRequest(response) def account_info(self) -> dict: @@ -217,10 +269,6 @@ def update_my_account( dict A dictionary containing your updated account information. """ - _header = self._auth_header - _header["accept"] = "*/*" - _header["content-type"] = "application/json" - _query: dict = {} if name: _query["name"] = name @@ -242,19 +290,11 @@ def update_my_account( logger.error(msg) raise Exception(msg) - response = r.patch( - url=self._ME_URL, - headers=_header, - data=json.dumps(_query), + return self.patch( + self._ME_URL, + data=_query, ) - if response.ok: - return response.json() - else: - msg = "Account could not be updated." - logger.error(msg) - raise Exception(msg) - def update_my_settings( self, active_team: str | None = None, @@ -271,10 +311,6 @@ def update_my_settings( dict The user settings dictionary following the change. """ - _header = self._auth_header - _header["accept"] = "*/*" - _header["content-type"] = "application/json" - _query: dict = {} if active_team: _query["activeTeam"] = active_team @@ -284,19 +320,11 @@ def update_my_settings( logger.error(msg) raise Exception(msg) - response = r.patch( - url=self._ME_URL + "/settings", - headers=_header, - data=json.dumps(_query), + return self.patch( + f"{self._ME_URL}/settings", + data=_query, ) - if response.ok: - return response.json() - else: - msg = "Account could not be updated." - logger.error(msg) - raise Exception(msg) - def get_my_recently_edited_charts( self, limit: str | int = 100, @@ -549,10 +577,7 @@ def update_description( number_divisor : str, optional A multiplier or divisor for the numbers """ - - _header = self._auth_header - _header["content-type"] = "application/json" - _data = { + _query = { "metadata": { "describe": { "source-name": source_name, @@ -567,18 +592,10 @@ def update_description( } } } - update_description_response = r.patch( - url=self._CHARTS_URL + f"/{chart_id}", - headers=_header, - data=json.dumps(_data), + return self.patch( + f"{self._CHARTS_URL}/{chart_id}", + data=_query, ) - if update_description_response.status_code == 200: - logger.debug("Chart updated!") - return None - else: - msg = f"Error. Status code: {update_description_response.status_code}" - logger.error(msg) - raise Exception(msg) def publish_chart(self, chart_id: str, display: bool = True) -> dict | HTML: """Publishes a chart, table or map. @@ -647,25 +664,10 @@ def update_metadata(self, chart_id: str, properties: dict) -> Any | None: properties : dict A python dictionary of properties to update. """ - _header = self._auth_header - _header["content-type"] = "application/json" - _data = {"metadata": properties} - - update_properties_response = r.patch( - url=self._CHARTS_URL + f"/{chart_id}", - headers=_header, - data=json.dumps(_data), + return self.patch( + f"{self._CHARTS_URL}/{chart_id}", + data={"metadata": properties}, ) - if update_properties_response.status_code == 200: - logger.debug("Chart's metadata updated!") - return None - else: - msg = f"Error. Status code: {update_properties_response.status_code}" - logger.error(msg) - text = json.loads(update_properties_response.text) - logger.debug("Message: ", text["message"]) - logger.debug("Chart could not be updated.") - raise Exception(msg) def update_chart( self, @@ -697,9 +699,6 @@ def update_chart( organization_id : str, optional New organization's ID, by default "" """ - _header = self._auth_header - _header["accept"] = "*/*" - _header["content-type"] = "application/json" _query = {} if title: _query["title"] = title @@ -714,18 +713,12 @@ def update_chart( if organization_id: _query["organizationId"] = organization_id - update_chart_response = r.patch( - url=self._CHARTS_URL + f"/{chart_id}", - headers=_header, - data=json.dumps(_query), + self.patch( + f"{self._CHARTS_URL}/{chart_id}", + data=_query, ) - if update_chart_response.status_code == 200: - logger.debug(f"Chart with id {chart_id} updated!") - return self.publish_chart(chart_id) - else: - msg = "Chart could not be updated at the time." - logger.debug(msg) - raise Exception(msg) + + return self.publish_chart(chart_id) def display_chart(self, chart_id: str) -> IPython.display.HTML: """Displays a datawrapper chart. @@ -1008,9 +1001,6 @@ def update_folder( dict A dictionary with the folder's updated metadata """ - _header = self._auth_header - _header["accept"] = "*/*" - _query: dict = {} if name: _query["name"] = name @@ -1021,22 +1011,11 @@ def update_folder( if user_id: _query["userId"] = user_id - url = self._FOLDERS_URL + f"/{folder_id}" - response = r.patch( - url=url, - headers=_header, - data=json.dumps(_query), + return self.patch( + f"{self._FOLDERS_URL}/{folder_id}", + data=_query, ) - if response.ok: - folder_info = response.json() - logger.debug(f"Folder {folder_id} updated") - return folder_info - else: - msg = "Folder could not be updated." - logger.error(msg) - raise Exception(msg) - def delete_folder(self, folder_id: str | int): """Delete an existing folder. @@ -1077,26 +1056,11 @@ def move_chart(self, chart_id: str, folder_id: str) -> Any | None: folder_id : str ID of folder to move visualization to. """ - - _header = self._auth_header - _header["content-type"] = "application/json" - - _data = {"folderId": folder_id} - - move_chart_response = r.patch( + return self.patch( url=self._CHARTS_URL + f"/{chart_id}", - headers=_header, - data=json.dumps(_data), + data={"folderId": folder_id}, ) - if move_chart_response.status_code == 200: - logger.debug(f"Chart moved to folder {folder_id}") - return None - else: - msg = "Chart could not be moved at the moment." - logger.error(msg) - raise Exception(msg) - def copy_chart(self, chart_id: str) -> dict: """Copy one of your charts, tables, or maps and create a new editable copy. @@ -1317,10 +1281,6 @@ def update_team( dict A dictionary with the team's updated metadata """ - _header = self._auth_header - _header["accept"] = "*/*" - _header["content-type"] = "application/json" - _query = {} if name: _query["name"] = name @@ -1332,21 +1292,11 @@ def update_team( logger.error(msg) raise Exception(msg) - response = r.patch( - url=self._TEAMS_URL + f"/{team_id}", - headers=_header, - data=json.dumps(_query), + return self.patch( + f"{self._TEAMS_URL}/{team_id}", + data=_query, ) - if response.ok: - team_info = response.json() - logger.debug(f"Team {team_id} updated") - return team_info - else: - msg = "Team could not be updated." - logger.error(msg) - raise Exception(msg) - def delete_team(self, team_id: str): """Delete an existing team. From 057a506d3be4f61a3ad7aa14994c5ed6af8a4786 Mon Sep 17 00:00:00 2001 From: palewire Date: Thu, 19 Oct 2023 16:51:14 -0400 Subject: [PATCH 4/6] mypy fixes --- datawrapper/__main__.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/datawrapper/__main__.py b/datawrapper/__main__.py index fe28be3..d93d1de 100644 --- a/datawrapper/__main__.py +++ b/datawrapper/__main__.py @@ -246,7 +246,7 @@ def update_my_account( language: str | None = None, password: str | None = None, old_password: str | None = None, - ): + ) -> dict: """Update your account information. Parameters @@ -298,7 +298,7 @@ def update_my_account( def update_my_settings( self, active_team: str | None = None, - ): + ) -> dict: """Update your account information. Parameters @@ -551,7 +551,7 @@ def update_description( number_append: str = "", number_format: str = "-", number_divisor: int = 0, - ) -> Any | None: + ) -> dict: """Update a chart's description. Parameters @@ -652,7 +652,7 @@ def chart_data(self, chart_id: str): """ return self.get(self._CHARTS_URL + f"/{chart_id}/data") - def update_metadata(self, chart_id: str, properties: dict) -> Any | None: + def update_metadata(self, chart_id: str, properties: dict) -> dict: """Update a chart, table, or map's metadata. Example: https://developer.datawrapper.de/docs/creating-a-chart-new#edit-colors @@ -678,7 +678,7 @@ def update_chart( language: str = "", folder_id: str = "", organization_id: str = "", - ) -> Any | None: + ) -> dict: """Updates a chart's title, theme, type, language, folder or organization. Parameters @@ -1046,7 +1046,7 @@ def delete_folder(self, folder_id: str | int): logger.error(msg) raise Exception(msg) - def move_chart(self, chart_id: str, folder_id: str) -> Any | None: + def move_chart(self, chart_id: str, folder_id: str) -> dict: """Moves a chart, table, or map to a specified folder. Parameters From f38ad98a0cb162890ce64a7ab16123464808c3fc Mon Sep 17 00:00:00 2001 From: palewire Date: Thu, 19 Oct 2023 16:54:08 -0400 Subject: [PATCH 5/6] mypy tweak --- datawrapper/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datawrapper/__main__.py b/datawrapper/__main__.py index d93d1de..94e11f9 100644 --- a/datawrapper/__main__.py +++ b/datawrapper/__main__.py @@ -678,7 +678,7 @@ def update_chart( language: str = "", folder_id: str = "", organization_id: str = "", - ) -> dict: + ) -> dict | HTML: """Updates a chart's title, theme, type, language, folder or organization. Parameters From 797dde7666995cf435f2c7c398d4f753141405d0 Mon Sep 17 00:00:00 2001 From: palewire Date: Fri, 20 Oct 2023 09:34:50 -0400 Subject: [PATCH 6/6] Add a common DELETE method --- datawrapper/__main__.py | 92 +++++++++++++++++++---------------------- 1 file changed, 43 insertions(+), 49 deletions(-) diff --git a/datawrapper/__main__.py b/datawrapper/__main__.py index 94e11f9..3d6e7cf 100644 --- a/datawrapper/__main__.py +++ b/datawrapper/__main__.py @@ -68,6 +68,37 @@ def __init__(self, access_token=_ACCESS_TOKEN): self._access_token = access_token self._auth_header = {"Authorization": f"Bearer {access_token}"} + def delete(self, url: str, timeout: int = 15) -> bool: + """Make a DELETE request to the Datawrapper API. + + Parameters + ---------- + url : str + The URL to request. + timeout : int, optional + The timeout for the request in seconds, by default 15 + + Returns + ------- + bool + Whether the request was successful. + """ + # Set the headers + headers = self._auth_header + headers["accept"] = "*/*" + + # Make the request + response = r.delete(url, headers=headers) + + # Handle the response + if response.ok: + return True + else: + logger.error( + f"Delete request failed with status code {response.status_code}." + ) + raise FailedRequest(response) + def get(self, url: str, params: dict | None = None, timeout: int = 15) -> Any: """Make a GET request to the Datawrapper API. @@ -1016,7 +1047,7 @@ def update_folder( data=_query, ) - def delete_folder(self, folder_id: str | int): + def delete_folder(self, folder_id: str | int) -> bool: """Delete an existing folder. Parameters @@ -1026,25 +1057,10 @@ def delete_folder(self, folder_id: str | int): Returns ------- - r.Response.content - The content of the requests.delete request + bool + True if the folder was deleted successfully. """ - _header = self._auth_header - _header["accept"] = "*/*" - - url = self._FOLDERS_URL + f"/{folder_id}" - response = r.delete( - url=url, - headers=_header, - ) - - if response.ok: - logger.debug(f"Folder {folder_id} deleted") - return response.content - else: - msg = "Folder could not be deleted." - logger.error(msg) - raise Exception(msg) + return self.delete(f"{self._FOLDERS_URL }/{folder_id}") def move_chart(self, chart_id: str, folder_id: str) -> dict: """Moves a chart, table, or map to a specified folder. @@ -1091,7 +1107,7 @@ def fork_chart(self, chart_id: str) -> dict: """ return self.post(f"{self._CHARTS_URL}/{chart_id}/fork") - def delete_chart(self, chart_id: str) -> r.Response.content: # type: ignore + def delete_chart(self, chart_id: str) -> bool: """Deletes a specified chart, table or map. Parameters @@ -1101,18 +1117,10 @@ def delete_chart(self, chart_id: str) -> r.Response.content: # type: ignore Returns ------- - r.Response.content - The content of the requests.delete + bool + True if the chart was deleted successfully. """ - - delete_chart_response = r.delete( - url=self._CHARTS_URL + f"/{chart_id}", headers=self._auth_header - ) - if delete_chart_response.content: - return delete_chart_response.content - else: - logger.debug(f"Successfully deleted chart with id {chart_id}") - return None + return self.delete(f"{self._CHARTS_URL}/{chart_id}") def get_charts( self, @@ -1297,7 +1305,7 @@ def update_team( data=_query, ) - def delete_team(self, team_id: str): + def delete_team(self, team_id: str) -> bool: """Delete an existing team. Parameters @@ -1307,21 +1315,7 @@ def delete_team(self, team_id: str): Returns ------- - r.Response.content - The content of the requests.delete + bool + True if team was deleted successfully. """ - _header = self._auth_header - _header["accept"] = "*/*" - - response = r.delete( - url=self._TEAMS_URL + f"/{team_id}", - headers=_header, - ) - - if response.ok: - logger.debug(f"Team {team_id} deleted") - return response.content - else: - msg = "Team could not be deleted." - logger.error(msg) - raise Exception(msg) + return self.delete(f"{self._TEAMS_URL}/{team_id}")