2424 ExecutionState ,
2525)
2626
27- from dune_client .query import Query
27+ from dune_client .query import QueryBase , DuneQuery
28+ from dune_client .types import QueryParameter
2829
2930
3031class DuneClient (DuneInterface , BaseDuneClient ):
@@ -34,6 +35,7 @@ class DuneClient(DuneInterface, BaseDuneClient):
3435 """
3536
3637 def _handle_response (self , response : Response ) -> Any :
38+ """Generic response handler utilized by all Dune API routes"""
3739 try :
3840 # Some responses can be decoded and converted to DuneErrors
3941 response_json = response .json ()
@@ -45,14 +47,15 @@ def _handle_response(self, response: Response) -> Any:
4547 raise ValueError ("Unreachable since previous line raises" ) from err
4648
4749 def _route_url (self , route : str ) -> str :
48- return f"{ self .BASE_URL } { self .API_PATH } { route } "
50+ return f"{ self .BASE_URL } { self .api_version } { route } "
4951
5052 def _get (
5153 self ,
5254 route : str ,
5355 params : Optional [Any ] = None ,
5456 raw : bool = False ,
5557 ) -> Any :
58+ """Generic interface for the GET method of a Dune API request"""
5659 url = self ._route_url (route )
5760 self .logger .debug (f"GET received input url={ url } " )
5861 response = requests .get (
@@ -65,7 +68,8 @@ def _get(
6568 return response
6669 return self ._handle_response (response )
6770
68- def _post (self , route : str , params : Any ) -> Any :
71+ def _post (self , route : str , params : Optional [Any ] = None ) -> Any :
72+ """Generic interface for the POST method of a Dune API request"""
6973 url = self ._route_url (route )
7074 self .logger .debug (f"POST received input url={ url } , params={ params } " )
7175 response = requests .post (
@@ -76,8 +80,21 @@ def _post(self, route: str, params: Any) -> Any:
7680 )
7781 return self ._handle_response (response )
7882
83+ def _patch (self , route : str , params : Any ) -> Any :
84+ """Generic interface for the PATCH method of a Dune API request"""
85+ url = self ._route_url (route )
86+ self .logger .debug (f"PATCH received input url={ url } , params={ params } " )
87+ response = requests .request (
88+ method = "PATCH" ,
89+ url = url ,
90+ json = params ,
91+ headers = {"x-dune-api-key" : self .token },
92+ timeout = self .DEFAULT_TIMEOUT ,
93+ )
94+ return self ._handle_response (response )
95+
7996 def execute (
80- self , query : Query , performance : Optional [str ] = None
97+ self , query : QueryBase , performance : Optional [str ] = None
8198 ) -> ExecutionResponse :
8299 """Post's to Dune API for execute `query`"""
83100 params = query .request_format ()
@@ -126,15 +143,15 @@ def get_result_csv(self, job_id: str) -> ExecutionResultCSV:
126143 response .raise_for_status ()
127144 return ExecutionResultCSV (data = BytesIO (response .content ))
128145
129- def get_latest_result (self , query : Union [Query , str , int ]) -> ResultsResponse :
146+ def get_latest_result (self , query : Union [QueryBase , str , int ]) -> ResultsResponse :
130147 """
131148 GET the latest results for a query_id without having to execute the query again.
132149
133150 :param query: :class:`Query` object OR query id as string | int
134151
135152 https://dune.com/docs/api/api-reference/latest_results/
136153 """
137- if isinstance (query , Query ):
154+ if isinstance (query , QueryBase ):
138155 params = {
139156 f"params.{ p .key } " : p .to_dict ()["value" ] for p in query .parameters ()
140157 }
@@ -167,7 +184,7 @@ def cancel_execution(self, job_id: str) -> bool:
167184
168185 def _refresh (
169186 self ,
170- query : Query ,
187+ query : QueryBase ,
171188 ping_frequency : int = 5 ,
172189 performance : Optional [str ] = None ,
173190 ) -> str :
@@ -191,7 +208,10 @@ def _refresh(
191208 return job_id
192209
193210 def refresh (
194- self , query : Query , ping_frequency : int = 5 , performance : Optional [str ] = None
211+ self ,
212+ query : QueryBase ,
213+ ping_frequency : int = 5 ,
214+ performance : Optional [str ] = None ,
195215 ) -> ResultsResponse :
196216 """
197217 Executes a Dune `query`, waits until execution completes,
@@ -204,7 +224,10 @@ def refresh(
204224 return self .get_result (job_id )
205225
206226 def refresh_csv (
207- self , query : Query , ping_frequency : int = 5 , performance : Optional [str ] = None
227+ self ,
228+ query : QueryBase ,
229+ ping_frequency : int = 5 ,
230+ performance : Optional [str ] = None ,
208231 ) -> ExecutionResultCSV :
209232 """
210233 Executes a Dune query, waits till execution completes,
@@ -217,7 +240,7 @@ def refresh_csv(
217240 return self .get_result_csv (job_id )
218241
219242 def refresh_into_dataframe (
220- self , query : Query , performance : Optional [str ] = None
243+ self , query : QueryBase , performance : Optional [str ] = None
221244 ) -> Any :
222245 """
223246 Execute a Dune Query, waits till execution completes,
@@ -233,3 +256,123 @@ def refresh_into_dataframe(
233256 ) from exc
234257 data = self .refresh_csv (query , performance = performance ).data
235258 return pandas .read_csv (data )
259+
260+ # CRUD Operations: https://dune.com/docs/api/api-reference/edit-queries/
261+ def create_query (
262+ self ,
263+ name : str ,
264+ query_sql : str ,
265+ params : Optional [list [QueryParameter ]] = None ,
266+ is_private : bool = False ,
267+ ) -> DuneQuery :
268+ """
269+ Creates Dune Query by ID
270+ https://dune.com/docs/api/api-reference/edit-queries/create-query/
271+ """
272+ parameters = {
273+ "name" : name ,
274+ "query_sql" : query_sql ,
275+ "private" : is_private ,
276+ }
277+ if params is not None :
278+ parameters ["parameters" ] = [p .to_dict () for p in params ]
279+ response_json = self ._post (route = "/query/" , params = parameters )
280+ try :
281+ query_id = int (response_json ["query_id" ])
282+ # Note that this requires an extra request.
283+ return self .get_query (query_id )
284+ except KeyError as err :
285+ raise DuneError (response_json , "create_query Response" , err ) from err
286+
287+ def get_query (self , query_id : int ) -> DuneQuery :
288+ """
289+ Retrieves Dune Query by ID
290+ https://dune.com/docs/api/api-reference/edit-queries/get-query/
291+ """
292+ response_json = self ._get (route = f"/query/{ query_id } " )
293+ return DuneQuery .from_dict (response_json )
294+
295+ def update_query ( # pylint: disable=too-many-arguments
296+ self ,
297+ query_id : int ,
298+ name : Optional [str ] = None ,
299+ query_sql : Optional [str ] = None ,
300+ params : Optional [list [QueryParameter ]] = None ,
301+ description : Optional [str ] = None ,
302+ tags : Optional [list [str ]] = None ,
303+ ) -> int :
304+ """
305+ Updates Dune Query by ID
306+ https://dune.com/docs/api/api-reference/edit-queries/update-query
307+
308+ The request body should contain all fields that need to be updated.
309+ Any omitted fields will be left untouched.
310+ If the tags or parameters are provided as an empty array,
311+ they will be deleted from the query.
312+ """
313+ parameters : dict [str , Any ] = {}
314+ if name is not None :
315+ parameters ["name" ] = name
316+ if description is not None :
317+ parameters ["description" ] = description
318+ if tags is not None :
319+ parameters ["tags" ] = tags
320+ if query_sql is not None :
321+ parameters ["query_sql" ] = query_sql
322+ if params is not None :
323+ parameters ["parameters" ] = [p .to_dict () for p in params ]
324+
325+ if not bool (parameters ):
326+ # Nothing to change no need to make reqeust
327+ self .logger .warning ("called update_query with no proposed changes." )
328+ return query_id
329+
330+ response_json = self ._patch (
331+ route = f"/query/{ query_id } " ,
332+ params = parameters ,
333+ )
334+ try :
335+ # No need to make a dataclass for this since it's just a boolean.
336+ return int (response_json ["query_id" ])
337+ except KeyError as err :
338+ raise DuneError (response_json , "update_query Response" , err ) from err
339+
340+ def archive_query (self , query_id : int ) -> bool :
341+ """
342+ https://dune.com/docs/api/api-reference/edit-queries/archive-query
343+ returns resulting value of Query.is_archived
344+ """
345+ response_json = self ._post (route = f"/query/{ query_id } /archive" )
346+ try :
347+ # No need to make a dataclass for this since it's just a boolean.
348+ return self .get_query (int (response_json ["query_id" ])).meta .is_archived
349+ except KeyError as err :
350+ raise DuneError (response_json , "make_private Response" , err ) from err
351+
352+ def unarchive_query (self , query_id : int ) -> bool :
353+ """
354+ https://dune.com/docs/api/api-reference/edit-queries/archive-query
355+ returns resulting value of Query.is_archived
356+ """
357+ response_json = self ._post (route = f"/query/{ query_id } /unarchive" )
358+ try :
359+ # No need to make a dataclass for this since it's just a boolean.
360+ return self .get_query (int (response_json ["query_id" ])).meta .is_archived
361+ except KeyError as err :
362+ raise DuneError (response_json , "make_private Response" , err ) from err
363+
364+ def make_private (self , query_id : int ) -> None :
365+ """
366+ https://dune.com/docs/api/api-reference/edit-queries/private-query
367+ returns resulting value of Query.is_private
368+ """
369+ response_json = self ._post (route = f"/query/{ query_id } /private" )
370+ assert self .get_query (int (response_json ["query_id" ])).meta .is_private
371+
372+ def make_public (self , query_id : int ) -> None :
373+ """
374+ https://dune.com/docs/api/api-reference/edit-queries/private-query
375+ returns resulting value of Query.is_private
376+ """
377+ response_json = self ._post (route = f"/query/{ query_id } /unprivate" )
378+ assert not self .get_query (int (response_json ["query_id" ])).meta .is_private
0 commit comments