llama-github 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,490 @@
1
+ # rag_processor.py
2
+ from llama_github.config.config import config
3
+ from llama_github.data_retrieval.github_api import GitHubAPIHandler
4
+ from llama_github.data_retrieval.github_entities import Repository
5
+ from llama_github.llm_integration.llm_handler import LLMManager, LLMHandler
6
+ from llama_github.logger import logger
7
+ from langchain_core.pydantic_v1 import BaseModel, Field
8
+ from typing import List, Optional, Dict
9
+ from langchain_text_splitters import Language, RecursiveCharacterTextSplitter
10
+ import json
11
+ import math
12
+ from numpy.linalg import norm
13
+ import numpy as np
14
+ import asyncio
15
+
16
+
17
+ class RAGProcessor:
18
+ def __init__(self, github_api_handler: GitHubAPIHandler, llm_manager: LLMManager = None, llm_handler: LLMHandler = None):
19
+ if llm_manager:
20
+ self.llm_manager = llm_manager
21
+ else:
22
+ self.llm_manager = LLMManager()
23
+
24
+ if llm_handler:
25
+ self.llm_handler = llm_handler
26
+ else:
27
+ self.llm_handler = LLMHandler(llm_manager=self.llm_manager)
28
+
29
+ self.github_api_handler = github_api_handler
30
+
31
+ class _LLMFirstGenenralAnswer(BaseModel):
32
+ question: str = Field(
33
+ ...,
34
+ description="The abstraction of user's question, only one sentence no more than 20 words.",
35
+ example="How to create a NumPy array in Python?"
36
+ )
37
+ answer: str = Field(
38
+ ...,
39
+ description="The answer to the user's question, better with sample code.",
40
+ example="You can use the `numpy.array` function to create a NumPy array in Python. The sample code is as follows:\n\n```python\nimport numpy as np\n\narray = np.array([1, 2, 3])\nprint(array)\n```"
41
+ )
42
+ code_search_logic: str = Field(
43
+ ...,
44
+ description="Simple logic analyze on how to search for Github code related to the user's question without detail search criteria nor keywords.",
45
+ )
46
+ issue_search_logic: str = Field(
47
+ ...,
48
+ description="Simple logic analyze on how to search for Github issues related to the user's question without detail search criteria nor keywords.",
49
+ )
50
+
51
+ async def analyze_question(self, query: str) -> List[str]:
52
+ """
53
+ analyze user's question and generate strategy for code search and issue search
54
+
55
+ Args:
56
+ query (str): user's initial question.
57
+
58
+ Returns:
59
+ str: the answer of question.
60
+ """
61
+ try:
62
+ logger.debug(
63
+ f"Analyzing question and generating strategy")
64
+ prompt = config.get("always_answer_prompt")
65
+ response = await self.llm_handler.ainvoke(human_question=query, prompt=prompt, output_structure=self._LLMFirstGenenralAnswer)
66
+ return [response.question, response.answer, response.code_search_logic, response.issue_search_logic]
67
+ except Exception as e:
68
+ logger.error(f"Error in analyzing question: {e}")
69
+ return [response.question, ""]
70
+
71
+ class _GitHubCodeSearchCriteria(BaseModel):
72
+ search_criteria: List[str] = Field(
73
+ ...,
74
+ description="A list of search criteria strings for GitHub code search, each following GitHub's search syntax.",
75
+ example=["NumPy Array language:python",
76
+ "log4j LoggingUtil language:java"],
77
+ min_items=1,
78
+ max_items=2
79
+ )
80
+
81
+ async def get_code_search_criteria(self, query: str, draft_answer: Optional[str] = None) -> List[str]:
82
+ """
83
+ generate Github search criteria based on user's question
84
+
85
+ Args:
86
+ query (str): user's initial question.
87
+
88
+ Returns:
89
+ str[]: the search criteria for Github code search.
90
+ """
91
+ try:
92
+ logger.debug(
93
+ f"Generating code search criteria for question: {query}")
94
+ prompt = config.get("code_search_criteria_prompt")
95
+ response = await self.llm_handler.ainvoke(human_question=query, prompt=prompt, context=[draft_answer] if draft_answer is not None else None, output_structure=self._GitHubCodeSearchCriteria)
96
+ logger.debug(
97
+ f"For {query}, the search_criterias for code search is: {response.search_criteria}")
98
+ return response.search_criteria
99
+ except Exception as e:
100
+ logger.error(f"Error in get_code_search_criteria: {e}")
101
+ return []
102
+
103
+ class _GitHubRepoSearchCriteria(BaseModel):
104
+ necessity_score: int = Field(
105
+ ...,
106
+ description="In case there is already Github Code search and issue search for question related context retrieve. How necessity do you think seperate repo search in Github will be required. 0-59:no necessity, 60-79:medium necessity, 80-100:high necessity",
107
+ example=65
108
+ )
109
+ search_criteria: List[str] = Field(
110
+ ...,
111
+ description="A list of search criteria strings for GitHub repository search, each following GitHub's search syntax. The sorting of the list should be based on the necessity of the search criteria.",
112
+ example=["NumPy Array language:python",
113
+ "spring-boot log4j language:java"],
114
+ min_items=0,
115
+ max_items=2
116
+ )
117
+
118
+ async def get_repo_search_criteria(self, query: str, draft_answer: Optional[str] = None) -> List[str]:
119
+ """
120
+ generate Github search criteria based on user's question
121
+
122
+ Args:
123
+ query (str): user's initial question.
124
+
125
+ Returns:
126
+ str[]: the search criteria for Github code search.
127
+ """
128
+ search_criteria = []
129
+ try:
130
+ logger.debug(
131
+ f"Generating repo search criteria for question: {query}")
132
+ prompt = config.get("repo_search_criteria_prompt")
133
+ response = await self.llm_handler.ainvoke(human_question=query, prompt=prompt, context=[draft_answer] if draft_answer is not None else None, output_structure=self._GitHubRepoSearchCriteria)
134
+ if response.necessity_score >= 80:
135
+ search_criteria = response.search_criteria
136
+ elif response.necessity_score >= 60 and len(response.search_criteria) >= 1:
137
+ search_criteria = response.search_criteria[:1]
138
+ logger.debug(
139
+ f"For {query}, the search_criterias for repo search is: {search_criteria} and repo search necessity score is {response.necessity_score}")
140
+ except Exception as e:
141
+ logger.error(f"Error in get_repo_search_criteria: {e}")
142
+ return search_criteria
143
+ return search_criteria
144
+
145
+ def get_repo_simple_structure(self, repo: Repository) -> str:
146
+ """
147
+ get a simple structure of a repository, only contains first 3 levels of repo folder/file structure.
148
+
149
+ Args:
150
+ repo (Repository): the repository object.
151
+
152
+ Returns:
153
+ json: the simple structure of the repository.
154
+ """
155
+ full_structure = repo.get_structure()
156
+
157
+ if not full_structure:
158
+ return json.dumps({})
159
+
160
+ def simplify_tree(tree, level=1):
161
+ """
162
+ Simplify the tree structure to keep only three levels deep.
163
+ """
164
+ if level > 3:
165
+ return '...'
166
+
167
+ simplified_tree = {}
168
+ for key, value in tree.items():
169
+ if 'children' in value:
170
+ simplified_tree[key] = {
171
+ 'children': simplify_tree(value['children'], level + 1)
172
+ }
173
+ else:
174
+ simplified_tree[key] = value
175
+ return simplified_tree
176
+
177
+ simplified_structure = simplify_tree(full_structure)
178
+ return json.dumps(simplified_structure, indent=4)
179
+
180
+ class _GitHubIssueSearchCriteria(BaseModel):
181
+ search_criteria: List[str] = Field(
182
+ ...,
183
+ description="A list of search criteria strings for GitHub issue search, each following GitHub's search syntax.",
184
+ example=["is:open label:bug Numpy Array",
185
+ "is:closed label:documentation langchain ollama"],
186
+ min_items=1,
187
+ max_items=2
188
+ )
189
+
190
+ async def get_issue_search_criteria(self, query: str, draft_answer: Optional[str] = None) -> List[str]:
191
+ """
192
+ generate Github search criteria based on user's question
193
+
194
+ Args:
195
+ query (str): user's initial question.
196
+
197
+ Returns:
198
+ str[]: the search criteria for Github issue search.
199
+ """
200
+ try:
201
+ logger.debug(
202
+ f"Generating issue search criteria for question: {query}")
203
+ prompt = config.get("issue_search_criteria_prompt")
204
+ response = await self.llm_handler.ainvoke(human_question=query, prompt=prompt, context=[draft_answer] if draft_answer is not None else None, output_structure=self._GitHubIssueSearchCriteria)
205
+ logger.debug(
206
+ f"For {query}, the search_criterias for issue search is: {response.search_criteria}")
207
+ return response.search_criteria
208
+ except Exception as e:
209
+ logger.error(f"Error in get_issue_search_criteria: {e}")
210
+ return []
211
+
212
+ def _arrange_code_search_result(self, code_search_result: List[Dict]) -> List[Dict]:
213
+ """
214
+ Arrange the result of code search into chunks suitable for LLM RAG contexts.
215
+
216
+ Args:
217
+ code_search_result (list): the result of code search.
218
+
219
+ Returns:
220
+ list: the arranged result of code search.
221
+ """
222
+ arranged_results = []
223
+
224
+ for result in code_search_result:
225
+ content = result['content']
226
+
227
+ # Split content into chunks
228
+ chunks = self._split_content_into_chunks(
229
+ content, language=result['language'] if 'language' in result else None)
230
+
231
+ for chunk in chunks:
232
+ repository_full_name = result.get(
233
+ 'repository_full_name', 'None')
234
+ description = result.get('description', 'None')
235
+ stargazers_count = result.get('stargazers_count', 'None')
236
+ updated_at = result.get('updated_at', 'None')
237
+ path = result.get('path', 'None')
238
+ language = result.get('language', 'None')
239
+
240
+ if updated_at != 'None':
241
+ updated_at = updated_at.strftime('%Y-%m-%d %H:%M:%S %Z')
242
+
243
+ chunk_text = (
244
+ f"Sample code from repository: {repository_full_name}\n"
245
+ f"repository description: {description}\n"
246
+ f"repository stars: {stargazers_count}\n"
247
+ f"repository last updated: {updated_at}\n"
248
+ f"code path in repository: {path}\n"
249
+ f"programming language is: {language}\n\n"
250
+ f"{chunk}"
251
+ )
252
+ arranged_results.append(chunk_text)
253
+
254
+ return arranged_results
255
+
256
+ def _split_content_into_chunks(self, content: str, language: Optional[str] = None, max_tokens: Optional[int] = config.get('chunk_size')) -> List[str]:
257
+ """
258
+ Split the content into chunks of maximum token length using LangChain's RecursiveCharacterTextSplitter.
259
+
260
+ Args:
261
+ content (str): The content to be split.
262
+ language (Optional[str]): The programming language of the code. Defaults to None.
263
+
264
+ Returns:
265
+ list: A list of content chunks.
266
+ """
267
+ chunk_overlap = math.ceil(max_tokens * 0.15 / 100) * 100
268
+
269
+ if language is None or language.lower() not in [e.value for e in Language] or language.lower() in ['markdown', 'html', 'c', 'perl']:
270
+ splitter = RecursiveCharacterTextSplitter.from_huggingface_tokenizer(
271
+ separators=[
272
+ "\n\n",
273
+ "\n",
274
+ "\r\n",
275
+ ],
276
+ chunk_size=max_tokens,
277
+ chunk_overlap=chunk_overlap,
278
+ tokenizer=self.llm_manager.tokenizer
279
+ )
280
+ else:
281
+ max_tokens = max_tokens * 5
282
+ chunk_overlap = chunk_overlap * 5
283
+ language_enum = Language[language.upper()]
284
+ splitter = RecursiveCharacterTextSplitter.from_language(
285
+ language=language_enum,
286
+ chunk_size=max_tokens,
287
+ chunk_overlap=chunk_overlap,
288
+ )
289
+
290
+ chunks = splitter.split_text(content)
291
+ return chunks
292
+
293
+ def _arrange_issue_search_result(self, issue_search_result: dict) -> dict:
294
+ """
295
+ arrange the result of issue search
296
+
297
+ Args:
298
+ issue_search_result (dict): the result of issue search.
299
+
300
+ Returns:
301
+ dict: the arranged result of issue search.
302
+ """
303
+ arranged_results = []
304
+
305
+ for result in issue_search_result:
306
+ content = result['issue_content']
307
+
308
+ # Split content into chunks
309
+ chunks = self._split_content_into_chunks(
310
+ content, max_tokens=config.get('issue_chunk_size'))
311
+
312
+ for chunk in chunks:
313
+ arranged_results.append(chunk)
314
+
315
+ return arranged_results
316
+
317
+ def _arrange_repo_search_result(self, repo_search_result: dict) -> dict:
318
+ """
319
+ arrange the result of repo search
320
+
321
+ Args:
322
+ repo_search_result (dict): the result of repo search.
323
+
324
+ Returns:
325
+ dict: the arranged result of repo search.
326
+ """
327
+ arranged_results = []
328
+
329
+ for result in repo_search_result:
330
+ content = result['content']
331
+
332
+ # Split content into chunks
333
+ chunks = self._split_content_into_chunks(
334
+ content, max_tokens=config.get('repo_chunk_size'))
335
+
336
+ for chunk in chunks:
337
+ arranged_results.append(chunk)
338
+
339
+ return arranged_results
340
+
341
+ def _arrange_google_search_result(self, google_search_result: dict) -> dict:
342
+ """
343
+ arrange the result of google search
344
+
345
+ Args:
346
+ google_search_result (dict): the result of google search.
347
+
348
+ Returns:
349
+ dict: the arranged result of google search.
350
+ """
351
+ arranged_results = []
352
+
353
+ for result in google_search_result:
354
+ content = result['content']
355
+
356
+ # Split content into chunks
357
+ chunks = self._split_content_into_chunks(
358
+ content, max_tokens=config.get('google_chunk_size'))
359
+
360
+ for chunk in chunks:
361
+ arranged_results.append(chunk)
362
+
363
+ return arranged_results
364
+
365
+ def arrange_context(self, code_search_result: Optional[dict] = None, issue_search_result: Optional[dict] = None, repo_search_result: Optional[dict] = None, google_search_result: Optional[dict] = None) -> dict:
366
+ """
367
+ arrange the context before RAG
368
+
369
+ Args:
370
+ code_search_result (dict, optional): the result of code search. Defaults to None.
371
+ issue_search_result (dict, optional): the result of issue search. Defaults to None.
372
+ repo_search_result (dict, optional): the result of repo search. Defaults to None.
373
+ google_search_result (dict, optional): the result of google search. Defaults to None.
374
+
375
+ Returns:
376
+ dict: the arranged context.
377
+ """
378
+ context = []
379
+ if code_search_result:
380
+ context.extend(
381
+ self._arrange_code_search_result(code_search_result))
382
+ if issue_search_result:
383
+ context.extend(
384
+ self._arrange_issue_search_result(issue_search_result))
385
+ if repo_search_result:
386
+ context.extend(
387
+ self._arrange_repo_search_result(repo_search_result))
388
+ if google_search_result:
389
+ context.extend(
390
+ self._arrange_google_search_result(google_search_result))
391
+ return context
392
+
393
+ async def retrieve_topn_contexts(self, context_list: List[str], query: str, answer: Optional[str] = None, top_n: Optional[int] = 5) -> List[str]:
394
+ """
395
+ Retrieve top n context strings from the context list.
396
+
397
+ Args:
398
+ context_list (List[str]): List of context strings to retrieve top n from.
399
+ query (str): The query string.
400
+ answer (Optional[str]): The answer string (optional).
401
+ top_n (Optional[int]): Number of top context strings to retrieve (default: 5).
402
+
403
+ Returns:
404
+ List[str]: A list of top n context strings.
405
+ """
406
+ top_contexts = []
407
+ try:
408
+ reranker = self.llm_manager.get_rerank_model()
409
+
410
+ sentence_pairs = [[query, doc] for doc in context_list]
411
+ rerank_scores = reranker.compute_score(sentence_pairs)
412
+
413
+ # Zip scores with contexts
414
+ scored_contexts = list(zip(rerank_scores, context_list))
415
+ sorted_scored_contexts = sorted(
416
+ scored_contexts, key=lambda x: x[0], reverse=True)
417
+
418
+ # Extract top 3*top_n contexts after rerank
419
+ selected_context = [context for score, context in sorted_scored_contexts[:min(
420
+ top_n*3, len(sorted_scored_contexts))]]
421
+
422
+ # If there are too few contexts, skip embedding comparison step
423
+ if len(selected_context) < top_n*2:
424
+ return selected_context[:min(top_n, len(selected_context))]
425
+
426
+ # Calculate embedding to select top 2*top_n
427
+ logger.debug("Embedding start...")
428
+ embedding_model = self.llm_manager.get_embedding_model()
429
+ query_embedding = embedding_model.encode(
430
+ query + "\n" + answer if answer is not None else "")
431
+ context_embeddings = [embedding_model.encode(
432
+ context) for context in selected_context]
433
+
434
+ cos_similarities = [(query_embedding @ context_embedding.T) / (norm(query_embedding) * norm(context_embedding))
435
+ for context_embedding in context_embeddings]
436
+
437
+ top_indices = np.argsort(cos_similarities)[-(top_n*2):][::-1]
438
+ top_contexts = [selected_context[i] for i in top_indices]
439
+ top_cos_similarities = [cos_similarities[i] for i in top_indices]
440
+ top_rerank_scores = [rerank_scores[context_list.index(
441
+ context)] for context in top_contexts]
442
+
443
+ # Use simple LLM to calculate context score
444
+ llm_scores = await asyncio.gather(*[self.get_context_relevance_score(query, context) for context in top_contexts])
445
+ logger.debug(f"Simple LLM scores: {llm_scores}")
446
+
447
+ # Combine scores for final ranking
448
+ combined_scores = [llm_score * cos_sim * rerank_score
449
+ for llm_score, cos_sim, rerank_score in zip(llm_scores, top_cos_similarities, top_rerank_scores)]
450
+
451
+ combined_context_scores = list(zip(top_contexts, combined_scores))
452
+ sorted_combined_context_scores = sorted(
453
+ combined_context_scores, key=lambda x: x[1], reverse=True)
454
+ logger.debug(
455
+ f"Combined sorted context scores: {sorted_combined_context_scores}")
456
+
457
+ top_contexts = [context for context,
458
+ _ in sorted_combined_context_scores[:top_n]]
459
+ logger.debug(f"Final top contexts: {top_contexts}")
460
+ except Exception as e:
461
+ logger.error(f"Error retrieving top n context: {e}")
462
+
463
+ return top_contexts
464
+
465
+ class _ContextRelevanceScore(BaseModel):
466
+ score: int = Field(
467
+ ...,
468
+ description="This is a Context Relevance Score, ranging from 0 to 100, indicates how well a given coding-related context supports answering a specific question, with higher scores signifying greater relevance."
469
+ )
470
+
471
+ async def get_context_relevance_score(self, query: str, context: str) -> int:
472
+ """
473
+ generate context relevance score based on user's question and provided context
474
+
475
+ Args:
476
+ query (str): user's initial question.
477
+ context (str): context fetched from Github
478
+
479
+ Returns:
480
+ int: context relevance score, from 0-100.
481
+ """
482
+ try:
483
+ prompt = config.get("scoring_context_prompt")
484
+ response = await self.llm_handler.ainvoke(human_question=query, prompt=prompt, context=[context], output_structure=self._ContextRelevanceScore, simple_llm=True)
485
+ logger.debug(
486
+ f"For {context[:20]}, the context relevance score is: {response.score}")
487
+ return response.score
488
+ except Exception as e:
489
+ logger.error(f"Error in get_context_relevance_score: {e}")
490
+ return -1
llama_github/utils.py ADDED
@@ -0,0 +1,98 @@
1
+ import hashlib
2
+ import base64
3
+ import re
4
+ import asyncio
5
+ import aiohttp
6
+ from typing import Optional, Dict, Any
7
+ from llama_github.logger import logger
8
+
9
+ class DataAnonymizer:
10
+ def __init__(self):
11
+ self.patterns = {
12
+ 'api_key': r'(?i)(api[_-]?key|sk[_-]live|sk[_-]test|sk[_-]prod|sk[_-]sandbox|openai[_-]?key)\s*[:=]\s*[\'"]?([a-zA-Z0-9-_]{20,})[\'"]?',
13
+ 'token': r'(?i)(token|access[_-]?token|auth[_-]?token|github[_-]?token|ghp_[a-zA-Z0-9]{36}|gho_[a-zA-Z0-9]{36}|ghu_[a-zA-Z0-9]{36}|ghr_[a-zA-Z0-9]{36}|ghs_[a-zA-Z0-9]{36})\s*[:=]\s*[\'"]?([a-zA-Z0-9-_]{20,})[\'"]?',
14
+ 'password': r'(?i)password\s*[:=]\s*[\'"]?([^\'"]+)[\'"]?',
15
+ 'email': r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}',
16
+ 'ip_address': r'\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b',
17
+ 'jwt': r'eyJ[a-zA-Z0-9-_]+\.eyJ[a-zA-Z0-9-_]+\.[a-zA-Z0-9-_]+',
18
+ 'phone_number': r'\+?[0-9]{1,4}?[-.\s]?(\(?\d{1,3}?\)?[-.\s]?)?\d{1,4}[-.\s]?\d{1,4}[-.\s]?\d{1,9}',
19
+ 'url': r'https?://(?:[-\w.]|(?:%[\da-fA-F]{2}))+',
20
+ 'credit_card': r'\b(?:\d[ -]*?){13,16}\b',
21
+ 'ssn': r'\b(?:\d[ -]*?){9}\b',
22
+ 'ipv6': r'(?i)([0-9a-f]{1,4}:){7}([0-9a-f]{1,4}|:)',
23
+ 'mac_address': r'(?i)([0-9a-f]{2}([:-]|$)){6}',
24
+ 'latitude_longitude': r'(?i)(lat|latitude|lon|longitude)\s*[:=]\s*[-+]?([0-9]*\.[0-9]+|[0-9]+),\s*[-+]?([0-9]*\.[0-9]+|[0-9]+)',
25
+ 'driver_license': r'(?i)([A-Z0-9]{1,20})\s*[:=]\s*([A-Z0-9]{1,20})',
26
+ 'date_of_birth': r'(?i)(dob|date[_-]?of[_-]?birth)\s*[:=]\s*([0-9]{4}-[0-9]{2}-[0-9]{2})',
27
+ 'name': r'(?i)(name|first[_-]?name|last[_-]?name)\s*[:=]\s*([a-zA-Z]{2,})',
28
+ 'address': r'(?i)(address|street[_-]?address)\s*[:=]\s*([a-zA-Z0-9\s,]{10,})',
29
+ 'zipcode': r'(?i)(zip|zipcode)\s*[:=]\s*([0-9]{5})',
30
+ 'company': r'(?i)(company|organization)\s*[:=]\s*([a-zA-Z\s]{2,})',
31
+ 'job_title': r'(?i)(job[_-]?title)\s*[:=]\s*([a-zA-Z\s]{2,})',
32
+ 'domain': r'(?i)(domain)\s*[:=]\s*([a-zA-Z0-9.-]{2,})',
33
+ 'hostname': r'(?i)(hostname)\s*[:=]\s*([a-zA-Z0-9.-]{2,})',
34
+ 'port': r'(?i)(port)\s*[:=]\s*([0-9]{2,})',
35
+ }
36
+
37
+ def hash_replacement(match):
38
+ sensitive_data = match.group(0)
39
+ hash_object = hashlib.sha256(sensitive_data.encode())
40
+ hashed_data = base64.urlsafe_b64encode(
41
+ hash_object.digest()).decode('utf-8')
42
+ return f'<ANONYMIZED:{hashed_data[:8]}>'
43
+
44
+ def anonymize_sensitive_data(self, question):
45
+ anonymized_question = question
46
+ for pattern_name, pattern in self.patterns.items():
47
+ anonymized_question = re.sub(
48
+ pattern, self.hash_replacement, anonymized_question)
49
+ return anonymized_question
50
+
51
+ class AsyncHTTPClient:
52
+ """
53
+ Asynchronous HTTP client class for sending asynchronous HTTP requests.
54
+ """
55
+
56
+ @staticmethod
57
+ async def request(
58
+ url: str,
59
+ method: str = "GET",
60
+ headers: Optional[Dict[str, str]] = None,
61
+ data: Optional[Dict[str, Any]] = None,
62
+ retry_count: int = 1,
63
+ retry_delay: int = 1,
64
+ ) -> Optional[aiohttp.ClientResponse]:
65
+ """
66
+ Send an asynchronous HTTP request.
67
+
68
+ :param url: The URL to send the request to.
69
+ :param method: The HTTP request method, default is "GET".
70
+ :param headers: The request headers, default is None.
71
+ :param data: The request data, default is None.
72
+ :param retry_count: The number of retries, default is 1.
73
+ :param retry_delay: The delay in seconds between each retry, default is 1.
74
+ :return: The response object if the request is successful, otherwise None.
75
+ """
76
+ async with aiohttp.ClientSession() as session:
77
+ for attempt in range(retry_count):
78
+ try:
79
+ async with session.request(
80
+ method, url, headers=headers, json=data
81
+ ) as response:
82
+ if response.status == 200:
83
+ return await response.json()
84
+ else:
85
+ logger.error(
86
+ f"Request failed with status code: {response.status}. "
87
+ f"Retrying ({attempt + 1}/{retry_count})..."
88
+ )
89
+ except aiohttp.ClientError as e:
90
+ logger.error(
91
+ f"Request failed with error: {str(e)}. "
92
+ f"Retrying ({attempt + 1}/{retry_count})..."
93
+ )
94
+
95
+ if attempt < retry_count - 1:
96
+ await asyncio.sleep(retry_delay)
97
+
98
+ return None
@@ -0,0 +1 @@
1
+ __version__ = '0.1.0'