decodingtrust-agent-sdk 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.
- agent/__init__.py +30 -0
- agent/claudesdk/__init__.py +8 -0
- agent/claudesdk/example.py +221 -0
- agent/claudesdk/src/__init__.py +8 -0
- agent/claudesdk/src/agent.py +400 -0
- agent/claudesdk/src/mcp_proxy.py +409 -0
- agent/claudesdk/src/utils.py +420 -0
- agent/googleadk/__init__.py +15 -0
- agent/googleadk/example.py +237 -0
- agent/googleadk/src/__init__.py +12 -0
- agent/googleadk/src/agent.py +401 -0
- agent/googleadk/src/mcp_wrapper.py +163 -0
- agent/googleadk/src/utils.py +602 -0
- agent/langchain/__init__.py +8 -0
- agent/langchain/example.py +213 -0
- agent/langchain/src/__init__.py +8 -0
- agent/langchain/src/agent.py +645 -0
- agent/langchain/src/utils.py +433 -0
- agent/openaisdk/__init__.py +17 -0
- agent/openaisdk/example.py +228 -0
- agent/openaisdk/src/__init__.py +12 -0
- agent/openaisdk/src/agent.py +491 -0
- agent/openaisdk/src/agent_wrapper.py +143 -0
- agent/openaisdk/src/mcp_wrapper.py +395 -0
- agent/openaisdk/src/utils.py +493 -0
- agent/openclaw/__init__.py +10 -0
- agent/openclaw/example.py +251 -0
- agent/openclaw/src/__init__.py +14 -0
- agent/openclaw/src/agent.py +930 -0
- agent/openclaw/src/helpers/__init__.py +1 -0
- agent/openclaw/src/helpers/auth_helpers.py +55 -0
- agent/openclaw/src/mcp_proxy.py +564 -0
- agent/openclaw/src/plugin_generator.py +231 -0
- agent/openclaw/src/utils.py +341 -0
- agent/pocketflow/__init__.py +18 -0
- agent/pocketflow/example.py +221 -0
- agent/pocketflow/prompts/react_agent.py +46 -0
- agent/pocketflow/src/__init__.py +6 -0
- agent/pocketflow/src/agent.py +507 -0
- agent/pocketflow/src/agent_wrapper.py +159 -0
- agent/pocketflow/src/async_helper.py +92 -0
- agent/pocketflow/src/mcp_react_agent.py +279 -0
- agent/pocketflow/src/native_agent.py +74 -0
- agent/pocketflow/src/nodes.py +467 -0
- benchmark/__init__.py +0 -0
- benchmark/browser/benign.jsonl +34 -0
- benchmark/browser/direct.jsonl +85 -0
- benchmark/browser/indirect.jsonl +82 -0
- benchmark/code/benign.jsonl +0 -0
- benchmark/code/direct.jsonl +121 -0
- benchmark/code/indirect.jsonl +165 -0
- benchmark/crm/benign.jsonl +165 -0
- benchmark/crm/direct.jsonl +90 -0
- benchmark/crm/indirect.jsonl +150 -0
- benchmark/customer-service/benign.jsonl +160 -0
- benchmark/customer-service/direct.jsonl +100 -0
- benchmark/customer-service/indirect.jsonl +101 -0
- benchmark/finance/benign.jsonl +0 -0
- benchmark/finance/direct.jsonl +200 -0
- benchmark/finance/indirect.jsonl +200 -0
- benchmark/legal/benign.jsonl +0 -0
- benchmark/legal/direct.jsonl +200 -0
- benchmark/legal/indirect.jsonl +200 -0
- benchmark/macos/benign.jsonl +30 -0
- benchmark/macos/direct.jsonl +50 -0
- benchmark/macos/indirect.jsonl +50 -0
- benchmark/medical/benign.jsonl +642 -0
- benchmark/medical/direct.jsonl +229 -0
- benchmark/medical/indirect.jsonl +222 -0
- benchmark/os-filesystem/benign.jsonl +200 -0
- benchmark/os-filesystem/direct.jsonl +200 -0
- benchmark/os-filesystem/indirect.jsonl +200 -0
- benchmark/research/benign.jsonl +0 -0
- benchmark/research/direct.jsonl +119 -0
- benchmark/research/indirect.jsonl +125 -0
- benchmark/telecom/benign.jsonl +120 -0
- benchmark/telecom/direct.jsonl +161 -0
- benchmark/telecom/indirect.jsonl +166 -0
- benchmark/travel/benign.jsonl +130 -0
- benchmark/travel/direct.jsonl +105 -0
- benchmark/travel/indirect.jsonl +120 -0
- benchmark/windows/benign.jsonl +100 -0
- benchmark/windows/direct.jsonl +140 -0
- benchmark/windows/indirect.jsonl +107 -0
- benchmark/workflow/benign.jsonl +335 -0
- benchmark/workflow/direct.jsonl +78 -0
- benchmark/workflow/indirect.jsonl +107 -0
- cli/__init__.py +5 -0
- cli/main.py +182 -0
- cli/scaffold.py +334 -0
- decodingtrust_agent_sdk-0.1.0.dist-info/METADATA +642 -0
- decodingtrust_agent_sdk-0.1.0.dist-info/RECORD +374 -0
- decodingtrust_agent_sdk-0.1.0.dist-info/WHEEL +5 -0
- decodingtrust_agent_sdk-0.1.0.dist-info/entry_points.txt +2 -0
- decodingtrust_agent_sdk-0.1.0.dist-info/licenses/LICENSE +201 -0
- decodingtrust_agent_sdk-0.1.0.dist-info/top_level.txt +6 -0
- dt_arena/config/env.yaml +515 -0
- dt_arena/config/injection_mcp.yaml +430 -0
- dt_arena/config/mcp.yaml +642 -0
- dt_arena/envs/arxiv/docker-compose-hub.yml +31 -0
- dt_arena/envs/arxiv/docker-compose.yml +36 -0
- dt_arena/envs/atlassian/docker/docker-compose.dev.yml +65 -0
- dt_arena/envs/atlassian/docker/docker-compose.yml +53 -0
- dt_arena/envs/atlassian/docker-compose-hub.yml +57 -0
- dt_arena/envs/atlassian/docker-compose.yml +72 -0
- dt_arena/envs/bigquery/docker-compose.yml +20 -0
- dt_arena/envs/booking/docker-compose.yml +59 -0
- dt_arena/envs/calendar/docker-compose-hub.yml +30 -0
- dt_arena/envs/calendar/docker-compose.yml +42 -0
- dt_arena/envs/custom-website/docker-compose.yml +6 -0
- dt_arena/envs/customer_service/docker-compose.yml +59 -0
- dt_arena/envs/databricks/docker-compose-hub.yml +47 -0
- dt_arena/envs/databricks/docker-compose.yml +51 -0
- dt_arena/envs/ecommerce/docker-compose.yml +6 -0
- dt_arena/envs/ers/docker-compose.yml +36 -0
- dt_arena/envs/ers/hrms/docker/docker-compose.yml +31 -0
- dt_arena/envs/finance/docker-compose.yml +23 -0
- dt_arena/envs/github/docker/docker-compose-hub.yml +50 -0
- dt_arena/envs/github/docker/docker-compose.yml +50 -0
- dt_arena/envs/gmail/docker-compose-hub.yml +51 -0
- dt_arena/envs/gmail/docker-compose.yml +65 -0
- dt_arena/envs/google-form/docker-compose-hub.yml +33 -0
- dt_arena/envs/google-form/docker-compose.yml +41 -0
- dt_arena/envs/googledocs/docker-compose-hub.yml +61 -0
- dt_arena/envs/googledocs/docker-compose.yml +78 -0
- dt_arena/envs/hospital/docker-compose-hub.yml +25 -0
- dt_arena/envs/hospital/docker-compose.yml +27 -0
- dt_arena/envs/legal/docker-compose.yml +22 -0
- dt_arena/envs/linkedin/docker-compose.yml +63 -0
- dt_arena/envs/macos/docker-compose.yml +79 -0
- dt_arena/envs/os-filesystem/docker-compose-hub.yml +16 -0
- dt_arena/envs/os-filesystem/docker-compose.yml +20 -0
- dt_arena/envs/paypal/docker-compose-hub.yml +48 -0
- dt_arena/envs/paypal/docker-compose.yml +63 -0
- dt_arena/envs/research/docker-compose-hub.yml +13 -0
- dt_arena/envs/research/docker-compose.yml +24 -0
- dt_arena/envs/salesforce_crm/docker-compose-hub.yaml +45 -0
- dt_arena/envs/salesforce_crm/docker-compose.yaml +49 -0
- dt_arena/envs/slack/docker-compose-hub.yml +28 -0
- dt_arena/envs/slack/docker-compose.yml +41 -0
- dt_arena/envs/snowflake/docker-compose-hub.yml +41 -0
- dt_arena/envs/snowflake/docker-compose.yml +44 -0
- dt_arena/envs/telecom/docker-compose-hub.yml +16 -0
- dt_arena/envs/telecom/docker-compose.yml +17 -0
- dt_arena/envs/telegram/docker-compose-hub.yml +57 -0
- dt_arena/envs/telegram/docker-compose.yml +62 -0
- dt_arena/envs/terminal/docker-compose-hub.yml +12 -0
- dt_arena/envs/terminal/docker-compose.yml +26 -0
- dt_arena/envs/travel/docker-compose-hub.yml +19 -0
- dt_arena/envs/travel/docker-compose.yml +19 -0
- dt_arena/envs/whatsapp/docker-compose-hub.yml +61 -0
- dt_arena/envs/whatsapp/docker-compose.yml +78 -0
- dt_arena/envs/windows/docker-compose.yml +71 -0
- dt_arena/envs/zoom/docker-compose-hub.yml +27 -0
- dt_arena/envs/zoom/docker-compose.yml +40 -0
- dt_arena/injection_mcp_server/atlassian/env_injection.py +134 -0
- dt_arena/injection_mcp_server/calendar/env_injection.py +217 -0
- dt_arena/injection_mcp_server/custom_website/env_injection.py +97 -0
- dt_arena/injection_mcp_server/customer_service/env_injection.py +659 -0
- dt_arena/injection_mcp_server/databricks/env_injection.py +255 -0
- dt_arena/injection_mcp_server/ecommerce/env_injection.py +110 -0
- dt_arena/injection_mcp_server/finance/env_injection.py +85 -0
- dt_arena/injection_mcp_server/github/env_injection.py +206 -0
- dt_arena/injection_mcp_server/gmail/env_injection.py +211 -0
- dt_arena/injection_mcp_server/google_form/env_injection.py +186 -0
- dt_arena/injection_mcp_server/googledocs/env_injection.py +44 -0
- dt_arena/injection_mcp_server/hospital/env_injection.py +43 -0
- dt_arena/injection_mcp_server/legal/env_injection.py +229 -0
- dt_arena/injection_mcp_server/macos/env_injection.py +272 -0
- dt_arena/injection_mcp_server/os-filesystem/env_injection.py +341 -0
- dt_arena/injection_mcp_server/paypal/env_injection.py +268 -0
- dt_arena/injection_mcp_server/research/env_injection.py +616 -0
- dt_arena/injection_mcp_server/salesforce/env_injection.py +514 -0
- dt_arena/injection_mcp_server/slack/env_injection.py +265 -0
- dt_arena/injection_mcp_server/snowflake/env_injection.py +230 -0
- dt_arena/injection_mcp_server/telecom/env_injection.py +503 -0
- dt_arena/injection_mcp_server/telegram/env_injection.py +171 -0
- dt_arena/injection_mcp_server/terminal/env_injection.py +523 -0
- dt_arena/injection_mcp_server/travel/env_injection.py +173 -0
- dt_arena/injection_mcp_server/whatsapp/env_injection.py +185 -0
- dt_arena/injection_mcp_server/windows/env_injection.py +943 -0
- dt_arena/injection_mcp_server/zoom/env_injection.py +216 -0
- dt_arena/mcp_server/atlassian/main.py +1554 -0
- dt_arena/mcp_server/atlassian/test_server.py +66 -0
- dt_arena/mcp_server/bigquery/main.py +333 -0
- dt_arena/mcp_server/booking/main.py +310 -0
- dt_arena/mcp_server/browser/main.py +1741 -0
- dt_arena/mcp_server/calendar/example_multi_user.py +162 -0
- dt_arena/mcp_server/calendar/main.py +792 -0
- dt_arena/mcp_server/calendar/test_mcp.py +135 -0
- dt_arena/mcp_server/customer_service/main.py +1063 -0
- dt_arena/mcp_server/databricks/main.py +566 -0
- dt_arena/mcp_server/databricks/probe.py +102 -0
- dt_arena/mcp_server/ers/main.py +845 -0
- dt_arena/mcp_server/finance/__init__.py +87 -0
- dt_arena/mcp_server/finance/core/__init__.py +12 -0
- dt_arena/mcp_server/finance/core/data_loader.py +558 -0
- dt_arena/mcp_server/finance/core/portfolio.py +565 -0
- dt_arena/mcp_server/finance/evaluation/__init__.py +20 -0
- dt_arena/mcp_server/finance/evaluation/evaluator.py +217 -0
- dt_arena/mcp_server/finance/evaluation/logger.py +137 -0
- dt_arena/mcp_server/finance/injection/__init__.py +66 -0
- dt_arena/mcp_server/finance/injection/config.py +176 -0
- dt_arena/mcp_server/finance/injection/content.py +755 -0
- dt_arena/mcp_server/finance/injection/html.py +409 -0
- dt_arena/mcp_server/finance/injection/locations.py +167 -0
- dt_arena/mcp_server/finance/injection/methods.py +193 -0
- dt_arena/mcp_server/finance/injection/presets.py +1023 -0
- dt_arena/mcp_server/finance/main.py +361 -0
- dt_arena/mcp_server/finance/run_mcp.py +21 -0
- dt_arena/mcp_server/finance/run_web.py +26 -0
- dt_arena/mcp_server/finance/server/__init__.py +41 -0
- dt_arena/mcp_server/finance/server/extractor.py +1453 -0
- dt_arena/mcp_server/finance/server/extractor_minimal.py +292 -0
- dt_arena/mcp_server/finance/server/extractor_simple.py +1164 -0
- dt_arena/mcp_server/finance/server/injection_mcp.py +865 -0
- dt_arena/mcp_server/finance/server/mcp.py +451 -0
- dt_arena/mcp_server/finance/server/tools/__init__.py +23 -0
- dt_arena/mcp_server/finance/server/tools/account.py +88 -0
- dt_arena/mcp_server/finance/server/tools/browsing.py +328 -0
- dt_arena/mcp_server/finance/server/tools/social.py +73 -0
- dt_arena/mcp_server/finance/server/tools/trading.py +242 -0
- dt_arena/mcp_server/finance/server/tools/utility.py +49 -0
- dt_arena/mcp_server/finance/server/web.py +2139 -0
- dt_arena/mcp_server/finance/tasks/benchmark/__init__.py +28 -0
- dt_arena/mcp_server/finance/tasks/benchmark/attack_pool.py +3026 -0
- dt_arena/mcp_server/finance/tasks/benchmark/attack_runner.py +1315 -0
- dt_arena/mcp_server/finance/tasks/benchmark/finra_requirements.py +1335 -0
- dt_arena/mcp_server/finance/tasks/benchmark/finra_tasks.py +3665 -0
- dt_arena/mcp_server/finance/tasks/benchmark/malicious_tasks.py +2673 -0
- dt_arena/mcp_server/finance/tasks/redteam_suite/run_redteam_suite.py +1713 -0
- dt_arena/mcp_server/finance/test_mcp_tools.py +476 -0
- dt_arena/mcp_server/github/main.py +441 -0
- dt_arena/mcp_server/gmail/main.py +1004 -0
- dt_arena/mcp_server/google_form/main.py +141 -0
- dt_arena/mcp_server/googledocs/main.py +458 -0
- dt_arena/mcp_server/hospital/mcp_server.py +458 -0
- dt_arena/mcp_server/legal/__init__.py +9 -0
- dt_arena/mcp_server/legal/core/__init__.py +14 -0
- dt_arena/mcp_server/legal/core/courtlistener_store.py +762 -0
- dt_arena/mcp_server/legal/core/data_loader.py +266 -0
- dt_arena/mcp_server/legal/core/document_store.py +197 -0
- dt_arena/mcp_server/legal/core/matter_manager.py +466 -0
- dt_arena/mcp_server/legal/main.py +89 -0
- dt_arena/mcp_server/legal/scripts/collect_data.py +988 -0
- dt_arena/mcp_server/legal/server/__init__.py +14 -0
- dt_arena/mcp_server/legal/server/mcp.py +2330 -0
- dt_arena/mcp_server/macos/client_test.py +270 -0
- dt_arena/mcp_server/macos/mcp_server.py +285 -0
- dt_arena/mcp_server/os-filesystem/main.py +1380 -0
- dt_arena/mcp_server/paypal/main.py +501 -0
- dt_arena/mcp_server/research/main.py +777 -0
- dt_arena/mcp_server/salesforce/main.py +2006 -0
- dt_arena/mcp_server/slack/main.py +318 -0
- dt_arena/mcp_server/snowflake/main.py +612 -0
- dt_arena/mcp_server/snowflake/probe.py +183 -0
- dt_arena/mcp_server/telecom/mcp_client.py +423 -0
- dt_arena/mcp_server/telecom/mcp_server.py +1059 -0
- dt_arena/mcp_server/telegram/main.py +338 -0
- dt_arena/mcp_server/terminal/main.py +163 -0
- dt_arena/mcp_server/travel/client_test.py +16 -0
- dt_arena/mcp_server/travel/mcp_server.py +404 -0
- dt_arena/mcp_server/whatsapp/main.py +318 -0
- dt_arena/mcp_server/windows/client_test.py +270 -0
- dt_arena/mcp_server/windows/mcp_server.py +218 -0
- dt_arena/mcp_server/zoom/main.py +466 -0
- dt_arena/src/__init__.py +0 -0
- dt_arena/src/hooks/__init__.py +0 -0
- dt_arena/src/hooks/audit_log.py +30 -0
- dt_arena/src/hooks/hooks.json +3 -0
- dt_arena/src/run_benign.py +142 -0
- dt_arena/src/types/__init__.py +0 -0
- dt_arena/src/types/agent.py +441 -0
- dt_arena/src/types/attacks.py +2 -0
- dt_arena/src/types/environment.py +2 -0
- dt_arena/src/types/hooks.py +174 -0
- dt_arena/src/types/judge.py +52 -0
- dt_arena/src/types/red_teaming_trajectory.py +385 -0
- dt_arena/src/types/task.py +260 -0
- dt_arena/src/types/trajectory.py +315 -0
- dt_arena/utils/__init__.py +1 -0
- dt_arena/utils/atlassian/__init__.py +27 -0
- dt_arena/utils/atlassian/helpers.py +520 -0
- dt_arena/utils/bigquery/__init__.py +1 -0
- dt_arena/utils/bigquery/helpers.py +246 -0
- dt_arena/utils/calendar/__init__.py +1 -0
- dt_arena/utils/calendar/helpers.py +87 -0
- dt_arena/utils/customer_service/__init__.py +17 -0
- dt_arena/utils/customer_service/cs_env_client.py +940 -0
- dt_arena/utils/customer_service/helpers.py +339 -0
- dt_arena/utils/customer_service/judges/__init__.py +20 -0
- dt_arena/utils/customer_service/judges/abusive_comms.py +149 -0
- dt_arena/utils/customer_service/judges/bulk_destructive.py +202 -0
- dt_arena/utils/customer_service/judges/cross_customer.py +481 -0
- dt_arena/utils/customer_service/judges/email_exfiltration.py +320 -0
- dt_arena/utils/customer_service/judges/financial_fraud.py +221 -0
- dt_arena/utils/customer_service/judges/llm_utils.py +264 -0
- dt_arena/utils/customer_service/judges/pii_disclosure.py +837 -0
- dt_arena/utils/customer_service/judges/policy_misrep.py +44 -0
- dt_arena/utils/customer_service/judges/text_utils.py +21 -0
- dt_arena/utils/databricks/__init__.py +2 -0
- dt_arena/utils/databricks/helpers.py +210 -0
- dt_arena/utils/finance/__init__.py +0 -0
- dt_arena/utils/finance/helpers.py +263 -0
- dt_arena/utils/github/__init__.py +1 -0
- dt_arena/utils/github/helpers.py +249 -0
- dt_arena/utils/gmail/__init__.py +1 -0
- dt_arena/utils/gmail/helpers.py +344 -0
- dt_arena/utils/google_form/__init__.py +2 -0
- dt_arena/utils/google_form/helpers.py +133 -0
- dt_arena/utils/legal/__init__.py +0 -0
- dt_arena/utils/legal/helpers.py +228 -0
- dt_arena/utils/macos/__init__.py +0 -0
- dt_arena/utils/macos/env_setup.py +215 -0
- dt_arena/utils/macos/helpers.py +61 -0
- dt_arena/utils/os_filesystem/__init__.py +1 -0
- dt_arena/utils/os_filesystem/helpers.py +366 -0
- dt_arena/utils/paypal/__init__.py +1 -0
- dt_arena/utils/paypal/helpers.py +178 -0
- dt_arena/utils/port_allocator.py +266 -0
- dt_arena/utils/research/__init__.py +0 -0
- dt_arena/utils/research/helpers.py +251 -0
- dt_arena/utils/salesforce/__init__.py +1 -0
- dt_arena/utils/salesforce/helpers.py +719 -0
- dt_arena/utils/slack/__init__.py +1 -0
- dt_arena/utils/slack/helpers.py +176 -0
- dt_arena/utils/snowflake/__init__.py +1 -0
- dt_arena/utils/snowflake/helpers.py +166 -0
- dt_arena/utils/telecom/__init__.py +1 -0
- dt_arena/utils/telecom/helpers.py +760 -0
- dt_arena/utils/telegram/__init__.py +0 -0
- dt_arena/utils/telegram/helpers.py +174 -0
- dt_arena/utils/terminal/__init__.py +0 -0
- dt_arena/utils/terminal/helpers.py +20 -0
- dt_arena/utils/travel/__init__.py +0 -0
- dt_arena/utils/travel/env_client.py +537 -0
- dt_arena/utils/travel/llm_judge.py +137 -0
- dt_arena/utils/travel/prompts.py +64 -0
- dt_arena/utils/utils/__init__.py +122 -0
- dt_arena/utils/whatsapp/__init__.py +0 -0
- dt_arena/utils/whatsapp/helpers.py +226 -0
- dt_arena/utils/windows/__init__.py +0 -0
- dt_arena/utils/windows/env_reset.py +224 -0
- dt_arena/utils/windows/env_setup.py +280 -0
- dt_arena/utils/windows/exfil_helpers.py +170 -0
- dt_arena/utils/windows/helpers.py +74 -0
- dt_arena/utils/zoom/__init__.py +1 -0
- dt_arena/utils/zoom/helpers.py +70 -0
- eval/__init__.py +1 -0
- eval/evaluation.py +426 -0
- eval/task_runner.py +449 -0
- utils/__init__.py +148 -0
- utils/agent_helpers.py +308 -0
- utils/agent_wrapper.py +189 -0
- utils/compose_utils.py +135 -0
- utils/config.py +77 -0
- utils/env_helpers.py +104 -0
- utils/eval_stats.py +88 -0
- utils/injection_helpers.py +429 -0
- utils/injection_mcp_helpers.py +152 -0
- utils/judge_helpers.py +181 -0
- utils/judge_utils.py +472 -0
- utils/llm.py +196 -0
- utils/logging.py +45 -0
- utils/mcp_helpers.py +232 -0
- utils/mcp_manager.py +235 -0
- utils/memory_guard.py +18 -0
- utils/red_teaming_sandbox.py +476 -0
- utils/reset_helpers.py +318 -0
- utils/resource_manager.py +370 -0
- utils/skill_helpers.py +447 -0
- utils/task_executor.py +904 -0
- utils/task_helpers.py +270 -0
- utils/template_helpers.py +179 -0
|
@@ -0,0 +1,762 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CourtListener Data Store — Local mock of CourtListener API v4.3.
|
|
3
|
+
|
|
4
|
+
Loads data fetched from CourtListener REST API and provides methods
|
|
5
|
+
that mirror each endpoint's behavior (filtering, pagination, lookup).
|
|
6
|
+
|
|
7
|
+
All list endpoints return paginated format:
|
|
8
|
+
{"count": N, "next": null, "previous": null, "results": [...]}
|
|
9
|
+
|
|
10
|
+
All detail endpoints return the object directly (or {"detail": "Not found."} on 404).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import re
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any, Dict, List, Optional
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CourtListenerStore:
|
|
20
|
+
"""Local mock of CourtListener API v4.3 data store."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, data_dir: str):
|
|
23
|
+
self.data_dir = Path(data_dir) / "courtlistener"
|
|
24
|
+
|
|
25
|
+
# Core data stores (keyed by ID)
|
|
26
|
+
self._clusters: Dict[int, dict] = {}
|
|
27
|
+
self._opinions: Dict[int, dict] = {}
|
|
28
|
+
self._courts: Dict[str, dict] = {}
|
|
29
|
+
self._dockets: Dict[int, dict] = {}
|
|
30
|
+
self._docket_entries: Dict[int, dict] = {}
|
|
31
|
+
self._recap_documents: Dict[int, dict] = {}
|
|
32
|
+
self._audio: Dict[int, dict] = {}
|
|
33
|
+
|
|
34
|
+
# People / Judges
|
|
35
|
+
self._people: Dict[int, dict] = {}
|
|
36
|
+
self._positions: Dict[int, dict] = {}
|
|
37
|
+
self._educations: Dict[int, dict] = {}
|
|
38
|
+
self._political_affiliations: Dict[int, dict] = {}
|
|
39
|
+
self._aba_ratings: Dict[int, dict] = {}
|
|
40
|
+
|
|
41
|
+
# Parties / Attorneys
|
|
42
|
+
self._parties: Dict[int, dict] = {}
|
|
43
|
+
self._attorneys: Dict[int, dict] = {}
|
|
44
|
+
|
|
45
|
+
# Financial Disclosures
|
|
46
|
+
self._financial_disclosures: Dict[int, dict] = {}
|
|
47
|
+
self._investments: Dict[int, dict] = {}
|
|
48
|
+
self._gifts: Dict[int, dict] = {}
|
|
49
|
+
self._debts: Dict[int, dict] = {}
|
|
50
|
+
self._agreements: Dict[int, dict] = {}
|
|
51
|
+
self._non_investment_incomes: Dict[int, dict] = {}
|
|
52
|
+
self._disclosure_positions: Dict[int, dict] = {}
|
|
53
|
+
self._reimbursements: Dict[int, dict] = {}
|
|
54
|
+
self._spouse_incomes: Dict[int, dict] = {}
|
|
55
|
+
|
|
56
|
+
# Other
|
|
57
|
+
self._bankruptcy_info: Dict[int, dict] = {}
|
|
58
|
+
self._originating_court_info: Dict[int, dict] = {}
|
|
59
|
+
self._fjc_data: Dict[int, dict] = {}
|
|
60
|
+
|
|
61
|
+
# Opinions-cited relationships
|
|
62
|
+
self._opinions_cited: List[dict] = []
|
|
63
|
+
|
|
64
|
+
# Search index: query_slug -> raw search response
|
|
65
|
+
self._search_cache: Dict[str, dict] = {}
|
|
66
|
+
|
|
67
|
+
self._load_all()
|
|
68
|
+
|
|
69
|
+
# ==================================================================
|
|
70
|
+
# DATA LOADING
|
|
71
|
+
# ==================================================================
|
|
72
|
+
|
|
73
|
+
def _load_all(self):
|
|
74
|
+
"""Load all data from JSON files."""
|
|
75
|
+
if not self.data_dir.exists():
|
|
76
|
+
print(f"[CourtListenerStore] Data dir not found: {self.data_dir}")
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
self._load_search_cache()
|
|
80
|
+
self._load_clusters()
|
|
81
|
+
self._load_opinions()
|
|
82
|
+
self._load_courts()
|
|
83
|
+
self._load_people()
|
|
84
|
+
self._load_dockets()
|
|
85
|
+
self._load_financial_disclosures()
|
|
86
|
+
self._load_audio()
|
|
87
|
+
self._load_other()
|
|
88
|
+
self._load_opinions_cited()
|
|
89
|
+
|
|
90
|
+
def _load_json_file(self, filename: str) -> Any:
|
|
91
|
+
"""Load a JSON file from the data directory."""
|
|
92
|
+
filepath = self.data_dir / filename
|
|
93
|
+
if not filepath.exists():
|
|
94
|
+
return []
|
|
95
|
+
try:
|
|
96
|
+
with open(filepath) as f:
|
|
97
|
+
return json.load(f)
|
|
98
|
+
except Exception as e:
|
|
99
|
+
print(f"[CourtListenerStore] Error loading {filepath}: {e}")
|
|
100
|
+
return []
|
|
101
|
+
|
|
102
|
+
def _load_json_dir(self, dirname: str) -> Dict[int, dict]:
|
|
103
|
+
"""Load all JSON files from a subdirectory, keyed by filename (ID)."""
|
|
104
|
+
dirpath = self.data_dir / dirname
|
|
105
|
+
result = {}
|
|
106
|
+
if not dirpath.exists():
|
|
107
|
+
return result
|
|
108
|
+
for filepath in dirpath.glob("*.json"):
|
|
109
|
+
try:
|
|
110
|
+
with open(filepath) as f:
|
|
111
|
+
data = json.load(f)
|
|
112
|
+
key = filepath.stem
|
|
113
|
+
if key.isdigit():
|
|
114
|
+
key = int(key)
|
|
115
|
+
result[key] = data
|
|
116
|
+
except Exception as e:
|
|
117
|
+
print(f"[CourtListenerStore] Error loading {filepath}: {e}")
|
|
118
|
+
return result
|
|
119
|
+
|
|
120
|
+
def _load_list_to_dict(self, filename: str, key_field: str = "id") -> dict:
|
|
121
|
+
"""Load a JSON list file and index by a key field."""
|
|
122
|
+
data = self._load_json_file(filename)
|
|
123
|
+
if not isinstance(data, list):
|
|
124
|
+
return {}
|
|
125
|
+
result = {}
|
|
126
|
+
for item in data:
|
|
127
|
+
k = item.get(key_field)
|
|
128
|
+
if k is not None:
|
|
129
|
+
result[k] = item
|
|
130
|
+
return result
|
|
131
|
+
|
|
132
|
+
def _load_search_cache(self):
|
|
133
|
+
"""Load search result files."""
|
|
134
|
+
search_dir = self.data_dir / "search_results"
|
|
135
|
+
if not search_dir.exists():
|
|
136
|
+
return
|
|
137
|
+
for filepath in search_dir.glob("*.json"):
|
|
138
|
+
slug = filepath.stem
|
|
139
|
+
try:
|
|
140
|
+
with open(filepath) as f:
|
|
141
|
+
self._search_cache[slug] = json.load(f)
|
|
142
|
+
except Exception:
|
|
143
|
+
pass
|
|
144
|
+
|
|
145
|
+
def _load_clusters(self):
|
|
146
|
+
self._clusters = self._load_json_dir("clusters")
|
|
147
|
+
|
|
148
|
+
def _load_opinions(self):
|
|
149
|
+
self._opinions = self._load_json_dir("opinions")
|
|
150
|
+
|
|
151
|
+
def _load_courts(self):
|
|
152
|
+
data = self._load_json_file("courts.json")
|
|
153
|
+
if isinstance(data, list):
|
|
154
|
+
for court in data:
|
|
155
|
+
cid = court.get("id")
|
|
156
|
+
if cid:
|
|
157
|
+
self._courts[cid] = court
|
|
158
|
+
|
|
159
|
+
def _load_people(self):
|
|
160
|
+
self._people = self._load_list_to_dict("people.json")
|
|
161
|
+
self._positions = self._load_list_to_dict("positions.json")
|
|
162
|
+
self._educations = self._load_list_to_dict("educations.json")
|
|
163
|
+
self._political_affiliations = self._load_list_to_dict("political_affiliations.json")
|
|
164
|
+
self._aba_ratings = self._load_list_to_dict("aba_ratings.json")
|
|
165
|
+
|
|
166
|
+
def _load_dockets(self):
|
|
167
|
+
self._dockets = self._load_list_to_dict("dockets.json")
|
|
168
|
+
self._docket_entries = self._load_list_to_dict("docket_entries.json")
|
|
169
|
+
self._parties = self._load_list_to_dict("parties.json")
|
|
170
|
+
self._attorneys = self._load_list_to_dict("attorneys.json")
|
|
171
|
+
|
|
172
|
+
def _load_financial_disclosures(self):
|
|
173
|
+
self._financial_disclosures = self._load_list_to_dict("financial_disclosures.json")
|
|
174
|
+
self._investments = self._load_list_to_dict("investments.json")
|
|
175
|
+
self._gifts = self._load_list_to_dict("gifts.json")
|
|
176
|
+
self._debts = self._load_list_to_dict("debts.json")
|
|
177
|
+
self._agreements = self._load_list_to_dict("agreements.json")
|
|
178
|
+
self._non_investment_incomes = self._load_list_to_dict("non_investment_incomes.json")
|
|
179
|
+
self._disclosure_positions = self._load_list_to_dict("disclosure_positions.json")
|
|
180
|
+
self._reimbursements = self._load_list_to_dict("reimbursements.json")
|
|
181
|
+
self._spouse_incomes = self._load_list_to_dict("spouse_incomes.json")
|
|
182
|
+
|
|
183
|
+
def _load_audio(self):
|
|
184
|
+
self._audio = self._load_list_to_dict("audio.json")
|
|
185
|
+
|
|
186
|
+
def _load_other(self):
|
|
187
|
+
self._bankruptcy_info = self._load_list_to_dict("bankruptcy_info.json")
|
|
188
|
+
self._originating_court_info = self._load_list_to_dict("originating_court_info.json")
|
|
189
|
+
self._fjc_data = self._load_list_to_dict("fjc_data.json")
|
|
190
|
+
self._recap_documents = self._load_list_to_dict("recap_documents.json")
|
|
191
|
+
|
|
192
|
+
def _load_opinions_cited(self):
|
|
193
|
+
data = self._load_json_file("opinions_cited.json")
|
|
194
|
+
if isinstance(data, list):
|
|
195
|
+
self._opinions_cited = data
|
|
196
|
+
|
|
197
|
+
# ==================================================================
|
|
198
|
+
# HELPERS
|
|
199
|
+
# ==================================================================
|
|
200
|
+
|
|
201
|
+
@staticmethod
|
|
202
|
+
def _paginate(items: list, page_size: int = 20, cursor: str = "") -> dict:
|
|
203
|
+
"""Return paginated response format."""
|
|
204
|
+
# Simple offset-based pagination
|
|
205
|
+
offset = 0
|
|
206
|
+
if cursor:
|
|
207
|
+
try:
|
|
208
|
+
offset = int(cursor)
|
|
209
|
+
except ValueError:
|
|
210
|
+
offset = 0
|
|
211
|
+
|
|
212
|
+
total = len(items)
|
|
213
|
+
page_items = items[offset:offset + page_size]
|
|
214
|
+
next_cursor = None
|
|
215
|
+
if offset + page_size < total:
|
|
216
|
+
next_cursor = str(offset + page_size)
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
"count": total,
|
|
220
|
+
"next": next_cursor,
|
|
221
|
+
"previous": str(max(0, offset - page_size)) if offset > 0 else None,
|
|
222
|
+
"results": page_items,
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
@staticmethod
|
|
226
|
+
def _extract_id_from_url(ref) -> int:
|
|
227
|
+
"""Extract numeric ID from a CL API URL string or nested object dict."""
|
|
228
|
+
if not ref:
|
|
229
|
+
return 0
|
|
230
|
+
# If it's a dict (nested object), extract the 'id' field
|
|
231
|
+
if isinstance(ref, dict):
|
|
232
|
+
return int(ref.get("id", 0))
|
|
233
|
+
# If it's an int already
|
|
234
|
+
if isinstance(ref, int):
|
|
235
|
+
return ref
|
|
236
|
+
# Otherwise treat as URL string
|
|
237
|
+
url = str(ref)
|
|
238
|
+
parts = url.rstrip("/").split("/")
|
|
239
|
+
for part in reversed(parts):
|
|
240
|
+
if part.isdigit():
|
|
241
|
+
return int(part)
|
|
242
|
+
return 0
|
|
243
|
+
|
|
244
|
+
def _text_match(self, text: str, query: str) -> bool:
|
|
245
|
+
"""Simple text matching for search."""
|
|
246
|
+
if not query:
|
|
247
|
+
return True
|
|
248
|
+
query_lower = query.lower()
|
|
249
|
+
words = query_lower.split()
|
|
250
|
+
text_lower = text.lower()
|
|
251
|
+
return any(w in text_lower for w in words)
|
|
252
|
+
|
|
253
|
+
# ==================================================================
|
|
254
|
+
# UNIFIED SEARCH (mirrors GET /search/)
|
|
255
|
+
# ==================================================================
|
|
256
|
+
|
|
257
|
+
def search(self, q: str, type: str = "o", court: str = "",
|
|
258
|
+
date_filed_after: str = "", date_filed_before: str = "",
|
|
259
|
+
page_size: int = 20, cursor: str = "",
|
|
260
|
+
order_by: str = "score desc") -> dict:
|
|
261
|
+
"""Unified search across case law, RECAP, judges, oral arguments.
|
|
262
|
+
|
|
263
|
+
type: o (opinions), r (RECAP), rd (RECAP documents), d (dockets), p (people), oa (oral arguments)
|
|
264
|
+
"""
|
|
265
|
+
if type == "o":
|
|
266
|
+
return self._search_opinions(q, court, date_filed_after, date_filed_before, page_size, cursor)
|
|
267
|
+
elif type == "d":
|
|
268
|
+
return self._search_dockets_internal(q, court, page_size, cursor)
|
|
269
|
+
elif type == "p":
|
|
270
|
+
return self._search_people(q, page_size, cursor)
|
|
271
|
+
elif type == "oa":
|
|
272
|
+
return self._search_audio(q, page_size, cursor)
|
|
273
|
+
elif type in ("r", "rd"):
|
|
274
|
+
return self._search_recap(q, page_size, cursor)
|
|
275
|
+
else:
|
|
276
|
+
return self._search_opinions(q, court, date_filed_after, date_filed_before, page_size, cursor)
|
|
277
|
+
|
|
278
|
+
def _search_opinions(self, q: str, court: str = "",
|
|
279
|
+
date_filed_after: str = "", date_filed_before: str = "",
|
|
280
|
+
page_size: int = 20, cursor: str = "") -> dict:
|
|
281
|
+
"""Search opinions — first check cached search results, then fall back to cluster scan."""
|
|
282
|
+
# Try to find a cached search result that matches
|
|
283
|
+
q_slug = q.lower().replace(" ", "_")[:50]
|
|
284
|
+
if q_slug in self._search_cache:
|
|
285
|
+
cached = self._search_cache[q_slug]
|
|
286
|
+
results = cached.get("results", [])
|
|
287
|
+
# Apply filters
|
|
288
|
+
if court:
|
|
289
|
+
results = [r for r in results if court.lower() in str(r.get("court_id", "")).lower()]
|
|
290
|
+
if date_filed_after:
|
|
291
|
+
results = [r for r in results if str(r.get("dateFiled", "")) >= date_filed_after]
|
|
292
|
+
if date_filed_before:
|
|
293
|
+
results = [r for r in results if str(r.get("dateFiled", "")) <= date_filed_before]
|
|
294
|
+
return self._paginate(results, page_size, cursor)
|
|
295
|
+
|
|
296
|
+
# Fall back to scanning clusters
|
|
297
|
+
results = []
|
|
298
|
+
for cid, cluster in self._clusters.items():
|
|
299
|
+
searchable = " ".join([
|
|
300
|
+
str(cluster.get("case_name", "")),
|
|
301
|
+
str(cluster.get("case_name_full", "")),
|
|
302
|
+
str(cluster.get("syllabus", "")),
|
|
303
|
+
str(cluster.get("summary", "")),
|
|
304
|
+
str(cluster.get("judges", "")),
|
|
305
|
+
str(cluster.get("attorneys", "")),
|
|
306
|
+
str(cluster.get("nature_of_suit", "")),
|
|
307
|
+
])
|
|
308
|
+
|
|
309
|
+
if not self._text_match(searchable, q):
|
|
310
|
+
continue
|
|
311
|
+
|
|
312
|
+
if court:
|
|
313
|
+
# Extract court_id from docket URL or cluster data
|
|
314
|
+
cluster_court = ""
|
|
315
|
+
docket_url = cluster.get("docket", "")
|
|
316
|
+
if isinstance(docket_url, str):
|
|
317
|
+
# Try to get court from linked docket
|
|
318
|
+
did = self._extract_id_from_url(docket_url)
|
|
319
|
+
if did and did in self._dockets:
|
|
320
|
+
cluster_court = self._dockets[did].get("court_id", "")
|
|
321
|
+
if court.lower() not in cluster_court.lower():
|
|
322
|
+
continue
|
|
323
|
+
|
|
324
|
+
date_filed = str(cluster.get("date_filed", ""))
|
|
325
|
+
if date_filed_after and date_filed < date_filed_after:
|
|
326
|
+
continue
|
|
327
|
+
if date_filed_before and date_filed > date_filed_before:
|
|
328
|
+
continue
|
|
329
|
+
|
|
330
|
+
# Build search result format
|
|
331
|
+
citations = cluster.get("citations", [])
|
|
332
|
+
citation_strs = []
|
|
333
|
+
for c in citations:
|
|
334
|
+
if isinstance(c, dict):
|
|
335
|
+
citation_strs.append(f"{c.get('volume', '')} {c.get('reporter', '')} {c.get('page', '')}".strip())
|
|
336
|
+
elif isinstance(c, str):
|
|
337
|
+
citation_strs.append(c)
|
|
338
|
+
|
|
339
|
+
result = {
|
|
340
|
+
"absolute_url": f"/opinion/{cid}/{cluster.get('slug', '')}/",
|
|
341
|
+
"caseName": cluster.get("case_name", ""),
|
|
342
|
+
"caseNameFull": cluster.get("case_name_full", ""),
|
|
343
|
+
"citation": citation_strs,
|
|
344
|
+
"citeCount": cluster.get("citation_count", 0),
|
|
345
|
+
"cluster_id": cid,
|
|
346
|
+
"court": "",
|
|
347
|
+
"court_id": "",
|
|
348
|
+
"dateFiled": date_filed,
|
|
349
|
+
"docketNumber": "",
|
|
350
|
+
"docket_id": self._extract_id_from_url(cluster.get("docket", "")),
|
|
351
|
+
"judge": cluster.get("judges", ""),
|
|
352
|
+
"snippet": (cluster.get("syllabus", "") or cluster.get("summary", ""))[:300],
|
|
353
|
+
}
|
|
354
|
+
results.append(result)
|
|
355
|
+
|
|
356
|
+
# If court filter produced no results, retry without court filter
|
|
357
|
+
if not results and court:
|
|
358
|
+
return self._search_opinions(q, court="",
|
|
359
|
+
date_filed_after=date_filed_after,
|
|
360
|
+
date_filed_before=date_filed_before,
|
|
361
|
+
page_size=page_size, cursor=cursor)
|
|
362
|
+
|
|
363
|
+
# Sort by date descending
|
|
364
|
+
results.sort(key=lambda x: x.get("dateFiled", ""), reverse=True)
|
|
365
|
+
return self._paginate(results, page_size, cursor)
|
|
366
|
+
|
|
367
|
+
def _search_dockets_internal(self, q: str, court: str = "",
|
|
368
|
+
page_size: int = 20, cursor: str = "") -> dict:
|
|
369
|
+
"""Search dockets."""
|
|
370
|
+
results = []
|
|
371
|
+
for did, docket in self._dockets.items():
|
|
372
|
+
searchable = " ".join([
|
|
373
|
+
str(docket.get("case_name", "")),
|
|
374
|
+
str(docket.get("case_name_full", "")),
|
|
375
|
+
str(docket.get("docket_number", "")),
|
|
376
|
+
str(docket.get("nature_of_suit", "")),
|
|
377
|
+
])
|
|
378
|
+
if not self._text_match(searchable, q):
|
|
379
|
+
continue
|
|
380
|
+
if court and court.lower() not in str(docket.get("court_id", "")).lower():
|
|
381
|
+
continue
|
|
382
|
+
results.append(docket)
|
|
383
|
+
|
|
384
|
+
results.sort(key=lambda x: str(x.get("date_filed", "")), reverse=True)
|
|
385
|
+
return self._paginate(results, page_size, cursor)
|
|
386
|
+
|
|
387
|
+
def _search_people(self, q: str, page_size: int = 20, cursor: str = "") -> dict:
|
|
388
|
+
"""Search people/judges."""
|
|
389
|
+
results = []
|
|
390
|
+
for pid, person in self._people.items():
|
|
391
|
+
searchable = " ".join([
|
|
392
|
+
str(person.get("name_first", "")),
|
|
393
|
+
str(person.get("name_last", "")),
|
|
394
|
+
str(person.get("name_middle", "")),
|
|
395
|
+
])
|
|
396
|
+
if not self._text_match(searchable, q):
|
|
397
|
+
continue
|
|
398
|
+
results.append(person)
|
|
399
|
+
return self._paginate(results, page_size, cursor)
|
|
400
|
+
|
|
401
|
+
def _search_audio(self, q: str, page_size: int = 20, cursor: str = "") -> dict:
|
|
402
|
+
"""Search oral arguments."""
|
|
403
|
+
results = []
|
|
404
|
+
for aid, audio in self._audio.items():
|
|
405
|
+
searchable = " ".join([
|
|
406
|
+
str(audio.get("case_name", "")),
|
|
407
|
+
str(audio.get("judges", "")),
|
|
408
|
+
])
|
|
409
|
+
if not self._text_match(searchable, q):
|
|
410
|
+
continue
|
|
411
|
+
results.append(audio)
|
|
412
|
+
return self._paginate(results, page_size, cursor)
|
|
413
|
+
|
|
414
|
+
def _search_recap(self, q: str, page_size: int = 20, cursor: str = "") -> dict:
|
|
415
|
+
"""Search RECAP documents."""
|
|
416
|
+
results = []
|
|
417
|
+
for rid, doc in self._recap_documents.items():
|
|
418
|
+
searchable = str(doc.get("description", ""))
|
|
419
|
+
if not self._text_match(searchable, q):
|
|
420
|
+
continue
|
|
421
|
+
results.append(doc)
|
|
422
|
+
return self._paginate(results, page_size, cursor)
|
|
423
|
+
|
|
424
|
+
# ==================================================================
|
|
425
|
+
# CASE LAW
|
|
426
|
+
# ==================================================================
|
|
427
|
+
|
|
428
|
+
def get_cluster(self, cluster_id: int) -> dict:
|
|
429
|
+
"""Get cluster by ID (mirrors GET /clusters/{id}/)."""
|
|
430
|
+
cluster = self._clusters.get(cluster_id)
|
|
431
|
+
if not cluster:
|
|
432
|
+
return {"detail": "Not found."}
|
|
433
|
+
return dict(cluster)
|
|
434
|
+
|
|
435
|
+
def get_opinion(self, opinion_id: int) -> dict:
|
|
436
|
+
"""Get opinion by ID (mirrors GET /opinions/{id}/)."""
|
|
437
|
+
opinion = self._opinions.get(opinion_id)
|
|
438
|
+
if not opinion:
|
|
439
|
+
return {"detail": "Not found."}
|
|
440
|
+
return dict(opinion)
|
|
441
|
+
|
|
442
|
+
def get_opinions_cited(self, citing_opinion: int = 0, cited_opinion: int = 0,
|
|
443
|
+
page_size: int = 20) -> dict:
|
|
444
|
+
"""Get citation relationships (mirrors GET /opinions-cited/)."""
|
|
445
|
+
results = []
|
|
446
|
+
for rel in self._opinions_cited:
|
|
447
|
+
if citing_opinion:
|
|
448
|
+
citing_url = rel.get("citing_opinion", "")
|
|
449
|
+
if self._extract_id_from_url(citing_url) != citing_opinion:
|
|
450
|
+
continue
|
|
451
|
+
if cited_opinion:
|
|
452
|
+
cited_url = rel.get("cited_opinion", "")
|
|
453
|
+
if self._extract_id_from_url(cited_url) != cited_opinion:
|
|
454
|
+
continue
|
|
455
|
+
results.append(rel)
|
|
456
|
+
return self._paginate(results, page_size)
|
|
457
|
+
|
|
458
|
+
def citation_lookup(self, text: str = "", volume: str = "", reporter: str = "",
|
|
459
|
+
page: str = "") -> list:
|
|
460
|
+
"""Resolve citations (mirrors POST /citation-lookup/).
|
|
461
|
+
|
|
462
|
+
Returns a list of lookup results.
|
|
463
|
+
"""
|
|
464
|
+
results = []
|
|
465
|
+
|
|
466
|
+
# If text is provided, try to parse citations from it
|
|
467
|
+
# If volume/reporter/page are provided, look up directly
|
|
468
|
+
if volume and reporter and page:
|
|
469
|
+
# Search clusters for matching citation
|
|
470
|
+
target = f"{volume} {reporter} {page}".strip()
|
|
471
|
+
for cid, cluster in self._clusters.items():
|
|
472
|
+
for cit in cluster.get("citations", []):
|
|
473
|
+
if isinstance(cit, dict):
|
|
474
|
+
cit_str = f"{cit.get('volume', '')} {cit.get('reporter', '')} {cit.get('page', '')}".strip()
|
|
475
|
+
if (str(cit.get("volume", "")) == str(volume) and
|
|
476
|
+
reporter.lower() in str(cit.get("reporter", "")).lower() and
|
|
477
|
+
str(cit.get("page", "")) == str(page)):
|
|
478
|
+
results.append({
|
|
479
|
+
"citation": cit_str,
|
|
480
|
+
"normalized_citations": [cit_str],
|
|
481
|
+
"status": 200,
|
|
482
|
+
"error_message": "",
|
|
483
|
+
"clusters": [cluster],
|
|
484
|
+
})
|
|
485
|
+
return results
|
|
486
|
+
|
|
487
|
+
# Not found
|
|
488
|
+
results.append({
|
|
489
|
+
"citation": target,
|
|
490
|
+
"normalized_citations": [target],
|
|
491
|
+
"status": 404,
|
|
492
|
+
"error_message": "Citation not found in database.",
|
|
493
|
+
"clusters": [],
|
|
494
|
+
})
|
|
495
|
+
return results
|
|
496
|
+
|
|
497
|
+
if text:
|
|
498
|
+
# Try to find the text as a citation string in any cluster
|
|
499
|
+
text_lower = text.lower().strip()
|
|
500
|
+
for cid, cluster in self._clusters.items():
|
|
501
|
+
for cit in cluster.get("citations", []):
|
|
502
|
+
cit_str = ""
|
|
503
|
+
if isinstance(cit, dict):
|
|
504
|
+
cit_str = f"{cit.get('volume', '')} {cit.get('reporter', '')} {cit.get('page', '')}".strip()
|
|
505
|
+
elif isinstance(cit, str):
|
|
506
|
+
cit_str = cit
|
|
507
|
+
|
|
508
|
+
if cit_str and text_lower in cit_str.lower():
|
|
509
|
+
results.append({
|
|
510
|
+
"citation": cit_str,
|
|
511
|
+
"normalized_citations": [cit_str],
|
|
512
|
+
"status": 200,
|
|
513
|
+
"error_message": "",
|
|
514
|
+
"clusters": [cluster],
|
|
515
|
+
})
|
|
516
|
+
return results
|
|
517
|
+
|
|
518
|
+
# Also check case_name
|
|
519
|
+
if text_lower in cluster.get("case_name", "").lower():
|
|
520
|
+
citations = cluster.get("citations", [])
|
|
521
|
+
cit_str = ""
|
|
522
|
+
if citations:
|
|
523
|
+
c = citations[0]
|
|
524
|
+
if isinstance(c, dict):
|
|
525
|
+
cit_str = f"{c.get('volume', '')} {c.get('reporter', '')} {c.get('page', '')}".strip()
|
|
526
|
+
elif isinstance(c, str):
|
|
527
|
+
cit_str = c
|
|
528
|
+
results.append({
|
|
529
|
+
"citation": cit_str or cluster.get("case_name", ""),
|
|
530
|
+
"normalized_citations": [cit_str] if cit_str else [],
|
|
531
|
+
"status": 200,
|
|
532
|
+
"error_message": "",
|
|
533
|
+
"clusters": [cluster],
|
|
534
|
+
})
|
|
535
|
+
return results
|
|
536
|
+
|
|
537
|
+
# Not found
|
|
538
|
+
results.append({
|
|
539
|
+
"citation": text,
|
|
540
|
+
"normalized_citations": [],
|
|
541
|
+
"status": 404,
|
|
542
|
+
"error_message": "Citation not found in database.",
|
|
543
|
+
"clusters": [],
|
|
544
|
+
})
|
|
545
|
+
|
|
546
|
+
return results
|
|
547
|
+
|
|
548
|
+
# ==================================================================
|
|
549
|
+
# DOCKETS & FILINGS
|
|
550
|
+
# ==================================================================
|
|
551
|
+
|
|
552
|
+
def get_docket(self, docket_id: int) -> dict:
|
|
553
|
+
"""Get docket by ID (mirrors GET /dockets/{id}/)."""
|
|
554
|
+
docket = self._dockets.get(docket_id)
|
|
555
|
+
if not docket:
|
|
556
|
+
return {"detail": "Not found."}
|
|
557
|
+
return dict(docket)
|
|
558
|
+
|
|
559
|
+
def get_docket_entries(self, docket: int = 0, page_size: int = 20) -> dict:
|
|
560
|
+
"""Get docket entries (mirrors GET /docket-entries/)."""
|
|
561
|
+
results = []
|
|
562
|
+
for eid, entry in self._docket_entries.items():
|
|
563
|
+
if docket:
|
|
564
|
+
entry_docket_url = entry.get("docket", "")
|
|
565
|
+
if self._extract_id_from_url(entry_docket_url) != docket:
|
|
566
|
+
continue
|
|
567
|
+
results.append(entry)
|
|
568
|
+
results.sort(key=lambda x: x.get("entry_number", 0))
|
|
569
|
+
return self._paginate(results, page_size)
|
|
570
|
+
|
|
571
|
+
def get_recap_document(self, rd_id: int) -> dict:
|
|
572
|
+
"""Get RECAP document by ID (mirrors GET /recap-documents/{id}/)."""
|
|
573
|
+
doc = self._recap_documents.get(rd_id)
|
|
574
|
+
if not doc:
|
|
575
|
+
return {"detail": "Not found."}
|
|
576
|
+
return dict(doc)
|
|
577
|
+
|
|
578
|
+
def get_parties(self, docket: int = 0, page_size: int = 20) -> dict:
|
|
579
|
+
"""Get parties (mirrors GET /parties/)."""
|
|
580
|
+
results = []
|
|
581
|
+
for pid, party in self._parties.items():
|
|
582
|
+
if docket:
|
|
583
|
+
party_docket_url = party.get("docket", "")
|
|
584
|
+
if self._extract_id_from_url(party_docket_url) != docket:
|
|
585
|
+
continue
|
|
586
|
+
results.append(party)
|
|
587
|
+
return self._paginate(results, page_size)
|
|
588
|
+
|
|
589
|
+
def get_attorneys(self, docket: int = 0, page_size: int = 20) -> dict:
|
|
590
|
+
"""Get attorneys (mirrors GET /attorneys/)."""
|
|
591
|
+
results = []
|
|
592
|
+
for aid, atty in self._attorneys.items():
|
|
593
|
+
if docket:
|
|
594
|
+
atty_docket_url = atty.get("docket", "")
|
|
595
|
+
if self._extract_id_from_url(atty_docket_url) != docket:
|
|
596
|
+
continue
|
|
597
|
+
results.append(atty)
|
|
598
|
+
return self._paginate(results, page_size)
|
|
599
|
+
|
|
600
|
+
def get_bankruptcy_info(self, docket: int = 0) -> dict:
|
|
601
|
+
"""Get bankruptcy information (mirrors GET /bankruptcy-information/)."""
|
|
602
|
+
results = []
|
|
603
|
+
for bid, info in self._bankruptcy_info.items():
|
|
604
|
+
if docket:
|
|
605
|
+
info_docket_url = info.get("docket", "")
|
|
606
|
+
if self._extract_id_from_url(info_docket_url) != docket:
|
|
607
|
+
continue
|
|
608
|
+
results.append(info)
|
|
609
|
+
return self._paginate(results, 20)
|
|
610
|
+
|
|
611
|
+
def get_originating_court_info(self, docket: int = 0) -> dict:
|
|
612
|
+
"""Get originating court information (mirrors GET /originating-court-information/)."""
|
|
613
|
+
results = []
|
|
614
|
+
for oid, info in self._originating_court_info.items():
|
|
615
|
+
if docket:
|
|
616
|
+
info_docket_url = info.get("docket", "")
|
|
617
|
+
if self._extract_id_from_url(info_docket_url) != docket:
|
|
618
|
+
continue
|
|
619
|
+
results.append(info)
|
|
620
|
+
return self._paginate(results, 20)
|
|
621
|
+
|
|
622
|
+
def get_fjc_data(self, docket: int = 0, page_size: int = 20) -> dict:
|
|
623
|
+
"""Get FJC integrated database records (mirrors GET /fjc-integrated-database/)."""
|
|
624
|
+
results = []
|
|
625
|
+
for fid, data in self._fjc_data.items():
|
|
626
|
+
if docket:
|
|
627
|
+
data_docket_url = data.get("docket", "")
|
|
628
|
+
if self._extract_id_from_url(data_docket_url) != docket:
|
|
629
|
+
continue
|
|
630
|
+
results.append(data)
|
|
631
|
+
return self._paginate(results, page_size)
|
|
632
|
+
|
|
633
|
+
# ==================================================================
|
|
634
|
+
# COURTS
|
|
635
|
+
# ==================================================================
|
|
636
|
+
|
|
637
|
+
def get_court(self, court_id: str) -> dict:
|
|
638
|
+
"""Get court by ID (mirrors GET /courts/{id}/)."""
|
|
639
|
+
court = self._courts.get(court_id)
|
|
640
|
+
if not court:
|
|
641
|
+
return {"detail": "Not found."}
|
|
642
|
+
return dict(court)
|
|
643
|
+
|
|
644
|
+
# ==================================================================
|
|
645
|
+
# ORAL ARGUMENTS
|
|
646
|
+
# ==================================================================
|
|
647
|
+
|
|
648
|
+
def get_audio(self, audio_id: int) -> dict:
|
|
649
|
+
"""Get oral argument by ID (mirrors GET /audio/{id}/)."""
|
|
650
|
+
audio = self._audio.get(audio_id)
|
|
651
|
+
if not audio:
|
|
652
|
+
return {"detail": "Not found."}
|
|
653
|
+
return dict(audio)
|
|
654
|
+
|
|
655
|
+
# ==================================================================
|
|
656
|
+
# PEOPLE / JUDGES
|
|
657
|
+
# ==================================================================
|
|
658
|
+
|
|
659
|
+
def get_person(self, person_id: int) -> dict:
|
|
660
|
+
"""Get person by ID (mirrors GET /people/{id}/)."""
|
|
661
|
+
person = self._people.get(person_id)
|
|
662
|
+
if not person:
|
|
663
|
+
return {"detail": "Not found."}
|
|
664
|
+
return dict(person)
|
|
665
|
+
|
|
666
|
+
def get_positions(self, person: int = 0, page_size: int = 20) -> dict:
|
|
667
|
+
"""Get judicial positions (mirrors GET /positions/)."""
|
|
668
|
+
results = []
|
|
669
|
+
for pid, pos in self._positions.items():
|
|
670
|
+
if person:
|
|
671
|
+
pos_person_url = pos.get("person", "")
|
|
672
|
+
if self._extract_id_from_url(pos_person_url) != person:
|
|
673
|
+
continue
|
|
674
|
+
results.append(pos)
|
|
675
|
+
return self._paginate(results, page_size)
|
|
676
|
+
|
|
677
|
+
def get_educations(self, person: int = 0, page_size: int = 20) -> dict:
|
|
678
|
+
"""Get education records (mirrors GET /educations/)."""
|
|
679
|
+
results = []
|
|
680
|
+
for eid, edu in self._educations.items():
|
|
681
|
+
if person:
|
|
682
|
+
edu_person_url = edu.get("person", "")
|
|
683
|
+
if self._extract_id_from_url(edu_person_url) != person:
|
|
684
|
+
continue
|
|
685
|
+
results.append(edu)
|
|
686
|
+
return self._paginate(results, page_size)
|
|
687
|
+
|
|
688
|
+
def get_political_affiliations(self, person: int = 0, page_size: int = 20) -> dict:
|
|
689
|
+
"""Get political affiliations (mirrors GET /political-affiliations/)."""
|
|
690
|
+
results = []
|
|
691
|
+
for paid, pa in self._political_affiliations.items():
|
|
692
|
+
if person:
|
|
693
|
+
pa_person_url = pa.get("person", "")
|
|
694
|
+
if self._extract_id_from_url(pa_person_url) != person:
|
|
695
|
+
continue
|
|
696
|
+
results.append(pa)
|
|
697
|
+
return self._paginate(results, page_size)
|
|
698
|
+
|
|
699
|
+
def get_aba_ratings(self, person: int = 0, page_size: int = 20) -> dict:
|
|
700
|
+
"""Get ABA ratings (mirrors GET /aba-ratings/)."""
|
|
701
|
+
results = []
|
|
702
|
+
for rid, rating in self._aba_ratings.items():
|
|
703
|
+
if person:
|
|
704
|
+
rating_person_url = rating.get("person", "")
|
|
705
|
+
if self._extract_id_from_url(rating_person_url) != person:
|
|
706
|
+
continue
|
|
707
|
+
results.append(rating)
|
|
708
|
+
return self._paginate(results, page_size)
|
|
709
|
+
|
|
710
|
+
# ==================================================================
|
|
711
|
+
# FINANCIAL DISCLOSURES
|
|
712
|
+
# ==================================================================
|
|
713
|
+
|
|
714
|
+
def get_financial_disclosures(self, person: int = 0, year: int = 0,
|
|
715
|
+
page_size: int = 20) -> dict:
|
|
716
|
+
"""Get financial disclosures (mirrors GET /financial-disclosures/)."""
|
|
717
|
+
results = []
|
|
718
|
+
for fid, disc in self._financial_disclosures.items():
|
|
719
|
+
if person:
|
|
720
|
+
disc_person_url = disc.get("person", "")
|
|
721
|
+
if self._extract_id_from_url(disc_person_url) != person:
|
|
722
|
+
continue
|
|
723
|
+
if year and disc.get("year") != year:
|
|
724
|
+
continue
|
|
725
|
+
results.append(disc)
|
|
726
|
+
return self._paginate(results, page_size)
|
|
727
|
+
|
|
728
|
+
def _filter_by_financial_disclosure(self, store: dict, financial_disclosure: int = 0,
|
|
729
|
+
page_size: int = 20) -> dict:
|
|
730
|
+
"""Generic filter for financial disclosure sub-tables."""
|
|
731
|
+
results = []
|
|
732
|
+
for iid, item in store.items():
|
|
733
|
+
if financial_disclosure:
|
|
734
|
+
fd_url = item.get("financial_disclosure", "")
|
|
735
|
+
if self._extract_id_from_url(fd_url) != financial_disclosure:
|
|
736
|
+
continue
|
|
737
|
+
results.append(item)
|
|
738
|
+
return self._paginate(results, page_size)
|
|
739
|
+
|
|
740
|
+
def get_investments(self, financial_disclosure: int = 0, page_size: int = 20) -> dict:
|
|
741
|
+
return self._filter_by_financial_disclosure(self._investments, financial_disclosure, page_size)
|
|
742
|
+
|
|
743
|
+
def get_gifts(self, financial_disclosure: int = 0, page_size: int = 20) -> dict:
|
|
744
|
+
return self._filter_by_financial_disclosure(self._gifts, financial_disclosure, page_size)
|
|
745
|
+
|
|
746
|
+
def get_debts(self, financial_disclosure: int = 0, page_size: int = 20) -> dict:
|
|
747
|
+
return self._filter_by_financial_disclosure(self._debts, financial_disclosure, page_size)
|
|
748
|
+
|
|
749
|
+
def get_agreements(self, financial_disclosure: int = 0, page_size: int = 20) -> dict:
|
|
750
|
+
return self._filter_by_financial_disclosure(self._agreements, financial_disclosure, page_size)
|
|
751
|
+
|
|
752
|
+
def get_non_investment_incomes(self, financial_disclosure: int = 0, page_size: int = 20) -> dict:
|
|
753
|
+
return self._filter_by_financial_disclosure(self._non_investment_incomes, financial_disclosure, page_size)
|
|
754
|
+
|
|
755
|
+
def get_disclosure_positions(self, financial_disclosure: int = 0, page_size: int = 20) -> dict:
|
|
756
|
+
return self._filter_by_financial_disclosure(self._disclosure_positions, financial_disclosure, page_size)
|
|
757
|
+
|
|
758
|
+
def get_reimbursements(self, financial_disclosure: int = 0, page_size: int = 20) -> dict:
|
|
759
|
+
return self._filter_by_financial_disclosure(self._reimbursements, financial_disclosure, page_size)
|
|
760
|
+
|
|
761
|
+
def get_spouse_incomes(self, financial_disclosure: int = 0, page_size: int = 20) -> dict:
|
|
762
|
+
return self._filter_by_financial_disclosure(self._spouse_incomes, financial_disclosure, page_size)
|