datamule 1.0.3__py3-none-any.whl → 1.0.6__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.
Files changed (43) hide show
  1. datamule/__init__.py +2 -13
  2. datamule/document.py +0 -1
  3. datamule/helper.py +85 -105
  4. datamule/portfolio.py +105 -29
  5. datamule/submission.py +0 -38
  6. {datamule-1.0.3.dist-info → datamule-1.0.6.dist-info}/METADATA +2 -8
  7. datamule-1.0.6.dist-info/RECORD +10 -0
  8. datamule/book/__init__.py +0 -0
  9. datamule/book/book.py +0 -34
  10. datamule/book/eftsquery.py +0 -127
  11. datamule/book/xbrl_retriever.py +0 -88
  12. datamule/data/company_former_names.csv +0 -8148
  13. datamule/data/company_metadata.csv +0 -10049
  14. datamule/data/company_tickers.csv +0 -9999
  15. datamule/data/sec-glossary.csv +0 -728
  16. datamule/data/xbrl_descriptions.csv +0 -10024
  17. datamule/downloader/downloader.py +0 -374
  18. datamule/downloader/premiumdownloader.py +0 -335
  19. datamule/mapping_dicts/txt_mapping_dicts.py +0 -234
  20. datamule/mapping_dicts/xml_mapping_dicts.py +0 -19
  21. datamule/monitor.py +0 -283
  22. datamule/mulebot/__init__.py +0 -1
  23. datamule/mulebot/helper.py +0 -35
  24. datamule/mulebot/mulebot.py +0 -130
  25. datamule/mulebot/mulebot_server/__init__.py +0 -1
  26. datamule/mulebot/mulebot_server/server.py +0 -87
  27. datamule/mulebot/mulebot_server/static/css/minimalist.css +0 -174
  28. datamule/mulebot/mulebot_server/static/scripts/artifacts.js +0 -68
  29. datamule/mulebot/mulebot_server/static/scripts/chat.js +0 -92
  30. datamule/mulebot/mulebot_server/static/scripts/filingArtifacts.js +0 -56
  31. datamule/mulebot/mulebot_server/static/scripts/listArtifacts.js +0 -15
  32. datamule/mulebot/mulebot_server/static/scripts/main.js +0 -57
  33. datamule/mulebot/mulebot_server/static/scripts/prefilledPrompt.js +0 -27
  34. datamule/mulebot/mulebot_server/static/scripts/suggestions.js +0 -47
  35. datamule/mulebot/mulebot_server/static/scripts/tableArtifacts.js +0 -129
  36. datamule/mulebot/mulebot_server/static/scripts/utils.js +0 -28
  37. datamule/mulebot/mulebot_server/templates/chat-minimalist.html +0 -91
  38. datamule/mulebot/search.py +0 -52
  39. datamule/mulebot/tools.py +0 -82
  40. datamule/packageupdater.py +0 -207
  41. datamule-1.0.3.dist-info/RECORD +0 -43
  42. {datamule-1.0.3.dist-info → datamule-1.0.6.dist-info}/WHEEL +0 -0
  43. {datamule-1.0.3.dist-info → datamule-1.0.6.dist-info}/top_level.txt +0 -0
datamule/monitor.py DELETED
@@ -1,283 +0,0 @@
1
- import asyncio
2
- import aiohttp
3
- from datetime import timedelta, datetime
4
- import pytz
5
- from collections import deque
6
- import time
7
- from .helper import headers, identifier_to_cik
8
-
9
- def _get_current_eastern_date():
10
- """Get current date in US Eastern timezone (automatically handles DST) """
11
- eastern = pytz.timezone('America/New_York')
12
- return datetime.now(eastern)
13
-
14
- def _parse_date(date_str):
15
- """Parse YYYY-MM-DD date string to datetime object in Eastern timezone"""
16
- try:
17
- date = datetime.strptime(date_str, '%Y-%m-%d')
18
- eastern = pytz.timezone('America/New_York')
19
- return eastern.localize(date)
20
- except ValueError as e:
21
- raise ValueError(f"Invalid date format. Please use YYYY-MM-DD. Error: {str(e)}")
22
-
23
- class PreciseRateLimiter:
24
- def __init__(self, rate, interval=1.0):
25
- self.rate = rate # requests per interval
26
- self.interval = interval # in seconds
27
- self.token_time = self.interval / self.rate # time per token
28
- self.last_time = time.time()
29
- self.lock = asyncio.Lock()
30
-
31
- async def acquire(self):
32
- async with self.lock:
33
- now = time.time()
34
- wait_time = self.last_time + self.token_time - now
35
- if wait_time > 0:
36
- await asyncio.sleep(wait_time)
37
- self.last_time = time.time()
38
- return True
39
-
40
- async def __aenter__(self):
41
- await self.acquire()
42
- return self
43
-
44
- async def __aexit__(self, exc_type, exc, tb):
45
- pass
46
-
47
- class RateMonitor:
48
- def __init__(self, window_size=1.0):
49
- self.window_size = window_size
50
- self.requests = deque()
51
- self._lock = asyncio.Lock()
52
-
53
- async def add_request(self, size_bytes):
54
- async with self._lock:
55
- now = time.time()
56
- self.requests.append((now, size_bytes))
57
- while self.requests and self.requests[0][0] < now - self.window_size:
58
- self.requests.popleft()
59
-
60
- def get_current_rates(self):
61
- now = time.time()
62
- while self.requests and self.requests[0][0] < now - self.window_size:
63
- self.requests.popleft()
64
-
65
- if not self.requests:
66
- return 0, 0
67
-
68
- request_count = len(self.requests)
69
- byte_count = sum(size for _, size in self.requests)
70
-
71
- requests_per_second = request_count / self.window_size
72
- mb_per_second = (byte_count / 1024 / 1024) / self.window_size
73
-
74
- return round(requests_per_second, 1), round(mb_per_second, 2)
75
-
76
- class Monitor:
77
- def __init__(self):
78
- self.last_total = 0
79
- self.last_date = None
80
- self.current_monitor_date = None
81
- self.submissions = []
82
- self.max_hits = 10000
83
- self.limiter = PreciseRateLimiter(5) # 5 requests per second
84
- self.rate_monitor = RateMonitor()
85
- self.headers = headers
86
-
87
- async def _fetch_json(self, session, url):
88
- """Fetch JSON with rate limiting and monitoring."""
89
- async with self.limiter:
90
- try:
91
- async with session.get(url) as response:
92
- response.raise_for_status()
93
- content = await response.read()
94
- await self.rate_monitor.add_request(len(content))
95
- return await response.json()
96
- except Exception as e:
97
- print(f"Error fetching {url}: {str(e)}")
98
- return None
99
-
100
- async def _poll(self, base_url, session, poll_interval, quiet):
101
- """Poll API until new submissions are found."""
102
- while True:
103
- current_date = _get_current_eastern_date()
104
-
105
- # If we're caught up to current date, use it, otherwise use our tracking date
106
- if self.current_monitor_date.date() >= current_date.date():
107
- self.current_monitor_date = current_date
108
- else:
109
- # If we're behind current date and haven't finished current date's processing,
110
- # continue with current date
111
- if self.last_date == self.current_monitor_date.strftime('%Y-%m-%d'):
112
- pass
113
- else:
114
- # Move to next day
115
- self.current_monitor_date += timedelta(days=1)
116
-
117
- date_str = self.current_monitor_date.strftime('%Y-%m-%d')
118
- timestamp = int(time.time())
119
-
120
- if self.last_date != date_str:
121
- print(f"Processing date: {date_str}")
122
- self.last_total = 0
123
- self.submissions = []
124
- self.last_date = date_str
125
-
126
- poll_url = f"{base_url}&startdt={date_str}&enddt={date_str}&v={timestamp}"
127
- if not quiet:
128
- print(f"Polling {poll_url}")
129
-
130
- try:
131
- data = await self._fetch_json(session, poll_url)
132
- if data:
133
- current_total = data['hits']['total']['value']
134
- if current_total > self.last_total:
135
- print(f"Found {current_total - self.last_total} new submissions for {date_str}")
136
- self.last_total = current_total
137
- return current_total, data, poll_url
138
- self.last_total = current_total
139
-
140
- # If we have no hits and we're processing a past date,
141
- # we can move to the next day immediately
142
- if current_total == 0 and self.current_monitor_date.date() < current_date.date():
143
- continue
144
-
145
- except Exception as e:
146
- print(f"Error in poll: {str(e)}")
147
-
148
- await asyncio.sleep(poll_interval / 1000)
149
-
150
- async def _retrieve_batch(self, session, poll_url, from_positions, quiet):
151
- """Retrieve a batch of submissions concurrently."""
152
- tasks = [
153
- self._fetch_json(
154
- session,
155
- f"{poll_url}&from={pos}"
156
- )
157
- for pos in from_positions
158
- ]
159
-
160
- results = await asyncio.gather(*tasks, return_exceptions=True)
161
- submissions = []
162
-
163
- for result in results:
164
- if isinstance(result, Exception):
165
- print(f"Error in batch: {str(result)}")
166
- continue
167
- if result and 'hits' in result:
168
- submissions.extend(result['hits']['hits'])
169
-
170
- return submissions
171
-
172
- async def _retrieve(self, poll_url, initial_data, session, quiet):
173
- """Retrieve all submissions using parallel batch processing."""
174
- batch_size = 10 # Number of concurrent requests
175
- page_size = 100 # Results per request
176
- max_position = min(self.max_hits, self.last_total)
177
- submissions = []
178
-
179
- # Process in batches of concurrent requests
180
- for batch_start in range(0, max_position, batch_size * page_size):
181
- from_positions = [
182
- pos for pos in range(
183
- batch_start,
184
- min(batch_start + batch_size * page_size, max_position),
185
- page_size
186
- )
187
- ]
188
-
189
- if not quiet:
190
- print(f"Retrieving batch from positions: {from_positions}")
191
-
192
- batch_submissions = await self._retrieve_batch(
193
- session, poll_url, from_positions, quiet
194
- )
195
-
196
- if not batch_submissions:
197
- break
198
-
199
- submissions.extend(batch_submissions)
200
-
201
- # If we got fewer results than expected, we're done
202
- if len(batch_submissions) < len(from_positions) * page_size:
203
- break
204
-
205
- return submissions
206
-
207
- async def _monitor(self, callback, form=None, cik=None, ticker=None, start_date=None, poll_interval=1000, quiet=True):
208
- """Main monitoring loop with parallel processing."""
209
- if poll_interval < 100:
210
- raise ValueError("SEC rate limit is 10 requests per second, set poll_interval to 100ms or higher")
211
-
212
- # Set up initial monitoring date
213
- if start_date:
214
- self.current_monitor_date = _parse_date(start_date)
215
- else:
216
- self.current_monitor_date = _get_current_eastern_date()
217
-
218
- # Handle form parameter
219
- if form is None:
220
- form = ['-0']
221
- elif isinstance(form, str):
222
- form = [form]
223
-
224
- # Handle CIK/ticker parameter
225
- cik_param = None
226
- if ticker is not None:
227
- cik_param = identifier_to_cik(ticker)
228
- elif cik is not None:
229
- cik_param = cik if isinstance(cik, list) else [cik]
230
-
231
- # Construct base URL
232
- base_url = 'https://efts.sec.gov/LATEST/search-index?forms=' + ','.join(form)
233
-
234
- # Add CIK parameter if specified
235
- if cik_param:
236
- cik_list = ','.join(str(c).zfill(10) for c in cik_param)
237
- base_url += f"&ciks={cik_list}"
238
-
239
- async with aiohttp.ClientSession(headers=self.headers) as session:
240
- while True:
241
- try:
242
- # Poll until we find new submissions
243
- _, data, poll_url = await self._poll(base_url, session, poll_interval, quiet)
244
-
245
- # Retrieve all submissions in parallel
246
- submissions = await self._retrieve(poll_url, data, session, quiet)
247
-
248
- # Find new submissions
249
- existing_ids = {sub['_id'] for sub in self.submissions}
250
- new_submissions = [
251
- sub for sub in submissions
252
- if sub['_id'] not in existing_ids
253
- ]
254
-
255
- if new_submissions:
256
- self.submissions.extend(new_submissions)
257
- if callback:
258
- await callback(new_submissions)
259
-
260
- reqs_per_sec, mb_per_sec = self.rate_monitor.get_current_rates()
261
- if not quiet:
262
- print(f"Current rates: {reqs_per_sec} req/s, {mb_per_sec} MB/s")
263
-
264
- except Exception as e:
265
- print(f"Error in monitor: {str(e)}")
266
- await asyncio.sleep(poll_interval / 1000)
267
-
268
- await asyncio.sleep(poll_interval / 1000)
269
-
270
- def monitor_submissions(self, callback=None, form=None, cik=None, ticker=None, start_date=None, poll_interval=1000, quiet=True):
271
- """
272
- Start the monitoring process.
273
-
274
- Parameters:
275
- callback (callable, optional): Function to call when new submissions are found
276
- form (str or list, optional): Form type(s) to monitor
277
- cik (str or list, optional): CIK(s) to monitor
278
- ticker (str, optional): Ticker symbol to monitor
279
- start_date (str, optional): Start date in YYYY-MM-DD format
280
- poll_interval (int, optional): Polling interval in milliseconds
281
- quiet (bool, optional): Suppress verbose output
282
- """
283
- asyncio.run(self._monitor(callback, form, cik, ticker, start_date, poll_interval, quiet))
@@ -1 +0,0 @@
1
- from .mulebot import MuleBot
@@ -1,35 +0,0 @@
1
- import requests
2
- from datamule.global_vars import headers
3
- from datamule.helper import identifier_to_cik
4
- from datamule import Parser
5
-
6
- parser = Parser()
7
-
8
- def get_company_concept(ticker):
9
-
10
- cik = identifier_to_cik(ticker)[0]
11
- url = f'https://data.sec.gov/api/xbrl/companyfacts/CIK{str(cik).zfill(10)}.json'
12
- response = requests.get(url,headers=headers)
13
- data = response.json()
14
-
15
- table_dict_list = parser.parse_company_concepts(data)
16
-
17
- # drop tables where label is None
18
- table_dict_list = [table_dict for table_dict in table_dict_list if table_dict['label'] is not None]
19
-
20
- return table_dict_list
21
-
22
- def select_dict_by_title(data, title):
23
- if isinstance(data, dict):
24
- if data.get('title') == title:
25
- return data
26
- for value in data.values():
27
- result = select_dict_by_title(value, title)
28
- if result:
29
- return result
30
- elif isinstance(data, list):
31
- for item in data:
32
- result = select_dict_by_title(item, title)
33
- if result:
34
- return result
35
- return None
@@ -1,130 +0,0 @@
1
- import openai
2
- import json
3
-
4
- from datamule.helper import identifier_to_cik
5
- from datamule import Downloader, Parser
6
- from .search import search_filing
7
- from .tools import tools, return_title_tool
8
- from .helper import get_company_concept, select_dict_by_title
9
-
10
- downloader = Downloader()
11
- parser = Parser()
12
-
13
-
14
- class MuleBot:
15
- def __init__(self, api_key):
16
- self.client = openai.OpenAI(api_key=api_key)
17
- self.messages = [
18
- {"role": "system", "content": "You are a helpful, but concise, assistant to assist with questions related to the Securities and Exchanges Commission. You are allowed to guess tickers."}
19
- ]
20
- self.total_tokens = 0
21
-
22
- def process_message(self, user_input):
23
-
24
- new_message_chain = self.messages
25
- new_message_chain.append({"role": "user", "content": user_input})
26
-
27
- try:
28
- response = self.client.chat.completions.create(
29
- model="gpt-4o-mini",
30
- messages=new_message_chain,
31
- tools=tools,
32
- tool_choice="auto"
33
- )
34
-
35
- self.total_tokens += response.usage.total_tokens
36
- assistant_message = response.choices[0].message
37
-
38
- if assistant_message.content is None:
39
- assistant_message.content = "I'm processing your request."
40
-
41
- new_message_chain.append({"role": "assistant", "content": assistant_message.content})
42
-
43
- tool_calls = assistant_message.tool_calls
44
- if tool_calls is None:
45
- return {'key':'text','value':assistant_message.content}
46
- else:
47
- for tool_call in tool_calls:
48
- print(f"Tool call: {tool_call.function.name}")
49
- if tool_call.function.name == "identifier_to_cik":
50
- function_args = json.loads(tool_call.function.arguments)
51
- print(f"Function args: {function_args}")
52
-
53
- cik = identifier_to_cik(function_args["ticker"])
54
- return {'key':'text','value':cik}
55
- elif tool_call.function.name == "get_company_concept":
56
- function_args = json.loads(tool_call.function.arguments)
57
- print(f"Function args: {function_args}")
58
- table_dict_list = get_company_concept(function_args["ticker"])
59
- return {'key':'table','value':table_dict_list}
60
- elif tool_call.function.name == "get_filing_urls":
61
- function_args = json.loads(tool_call.function.arguments)
62
- print(f"Function args: {function_args}")
63
- result = downloader.download(**function_args,return_urls=True)
64
- return {'key':'list','value':result}
65
- elif tool_call.function.name == "find_filing_section_by_title":
66
- function_args = json.loads(tool_call.function.arguments)
67
- print(f"Function args: {function_args}")
68
- # Parse the filing
69
- data = parser.parse_filing(function_args["url"])
70
-
71
- # find possible matches
72
- section_dicts = search_filing(query = function_args["title"], nested_dict =data, score_cutoff=0.3)
73
-
74
- # feed titles back to assistant
75
- titles = [section['title'] for section in section_dicts]
76
- new_message_chain.append({"role": "assistant", "content": f"Which of these titles is closest: {','.join(titles)}"})
77
-
78
- title_response = self.client.chat.completions.create(
79
- model="gpt-4o-mini",
80
- messages=new_message_chain,
81
- tools=[return_title_tool],
82
- tool_choice="required"
83
- )
84
-
85
- title_tool_call = title_response.choices[0].message.tool_calls[0]
86
- title = json.loads(title_tool_call.function.arguments)['title']
87
- print(f"Selected title: {title}")
88
- #print(f"Possible titles: {titles}")
89
-
90
- # select the section
91
- #section_dict = select_dict_by_title(data, title)
92
-
93
- # probably want to return full dict, and section label
94
- return {'key':'filing','value':{'data':data,'title':title}}
95
-
96
- return {'key':'text','value':'No tool call was made.'}
97
-
98
- except Exception as e:
99
- return f"An error occurred: {str(e)}"
100
-
101
- def get_total_tokens(self):
102
- return self.total_tokens
103
-
104
- def run(self):
105
- """Basic chatbot loop"""
106
- print("MuleBot: Hello! I'm here to assist you with questions related to the Securities and Exchange Commission. Type 'quit', 'exit', or 'bye' to end the conversation.")
107
- while True:
108
- user_input = input("You: ")
109
- if user_input.lower() in ['quit', 'exit', 'bye']:
110
- print("MuleBot: Goodbye!")
111
- break
112
-
113
- response = self.process_message(user_input)
114
- response_type = response['key']
115
-
116
- if response_type == 'text':
117
- value = response['value']
118
- print(value)
119
- elif response_type == 'table':
120
- value = response['value']
121
- print(value)
122
- elif response_type == 'list':
123
- value = response['value']
124
- print(value)
125
- elif response_type == 'filing':
126
- value = response['value']
127
- print(value)
128
- else:
129
- value = response['value']
130
- print(value)
@@ -1 +0,0 @@
1
- from .server import MuleBotServer
@@ -1,87 +0,0 @@
1
- import os
2
- from flask import Flask, request, jsonify, render_template
3
- from datamule.mulebot import MuleBot
4
- from datamule.filing_viewer import create_interactive_filing, create_valid_id
5
-
6
- class MuleBotServer:
7
- def __init__(self, template='chat-minimalist.html'):
8
- template_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), 'templates'))
9
- static_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), 'static'))
10
- self.app = Flask(__name__, template_folder=template_dir, static_folder=static_dir)
11
- self.mulebot = None
12
- self.template = template
13
- self.setup_routes()
14
-
15
- def setup_routes(self):
16
- @self.app.route('/')
17
- def home():
18
- return render_template(self.template)
19
-
20
- @self.app.route('/chat-with-prompt')
21
- def chat_with_prompt():
22
- prefilled_prompt = request.args.get('prompt', '')
23
- return render_template(self.template, prefilled_prompt=prefilled_prompt)
24
-
25
- @self.app.route('/chat', methods=['POST'])
26
- def chat():
27
- user_input = request.json['message']
28
-
29
- # Process the message using MuleBot's process_message method
30
- response = self.mulebot.process_message(user_input)
31
- response_type = response['key']
32
-
33
- # Prepare the response based on the type
34
- if response_type == 'text':
35
- # If response type is text, add it to the chat
36
- chat_response = {
37
- 'type': 'text',
38
- 'content': response['value']
39
- }
40
- elif response_type == 'table':
41
- # If response type is table, prepare it for the artifact window
42
- chat_response = {
43
- 'type': 'artifact',
44
- 'content': response['value'],
45
- 'artifact_type': 'artifact-table'
46
- }
47
- elif response_type == 'list':
48
- chat_response = {
49
- 'type': 'artifact',
50
- 'content': response['value'],
51
- 'artifact_type': 'artifact-list'
52
- }
53
- elif response_type == 'filing':
54
- data = response['value']['data']
55
- title = response['value']['title']
56
- section_id = create_valid_id(title)
57
-
58
- # create a filing viewer display
59
- html = create_interactive_filing(data)
60
-
61
- # we'll need to display the filing viewer in the artifact window, with a json export option
62
- chat_response = {
63
- 'type': 'artifact',
64
- 'content': html,
65
- 'data': data,
66
- 'section_id': section_id,
67
- 'artifact_type': 'artifact-filing'
68
- }
69
- else:
70
- # Handle other types of responses if needed
71
- chat_response = {
72
- 'type': 'unknown',
73
- 'content': 'Unsupported response type'
74
- }
75
-
76
- return jsonify({
77
- 'response': chat_response,
78
- 'total_tokens': self.mulebot.get_total_tokens()
79
- })
80
-
81
- def set_api_key(self, api_key):
82
- self.mulebot = MuleBot(api_key)
83
-
84
- def run(self, debug=False, host='0.0.0.0', port=5000):
85
- if not self.mulebot:
86
- raise ValueError("API key not set. Please call set_api_key() before running the server.")
87
- self.app.run(debug=debug, host=host, port=port)