• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

scope3data / scope3ai-py / 12840122239

18 Jan 2025 02:37AM UTC coverage: 95.824% (+15.3%) from 80.557%
12840122239

Pull #68

github

308311
tito
fix(huggingface): fixing aiohttp not working with VCR if passing filename
Pull Request #68: fix(huggingface): fixing aiohttp not working with VCR if passing filename

3 of 3 new or added lines in 2 files covered. (100.0%)

45 existing lines in 9 files now uncovered.

2111 of 2203 relevant lines covered (95.82%)

3.83 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.23
/scope3ai/lib.py
1
import importlib.metadata
4✔
2
import importlib.util
4✔
3
import logging
4✔
4
from contextlib import contextmanager
4✔
5
from contextvars import ContextVar
4✔
6
from functools import partial
4✔
7
from os import getenv
4✔
8
from typing import Optional, List
4✔
9
from uuid import uuid4
4✔
10
import atexit
4✔
11

12
from .api.client import Client, AsyncClient
4✔
13
from .api.tracer import Tracer
4✔
14
from .api.types import ImpactRow, ImpactResponse, Scope3AIContext
4✔
15
from .api.defaults import DEFAULT_API_URL
4✔
16
from .constants import PROVIDERS
4✔
17

18
from .worker import BackgroundWorker
4✔
19

20
logger = logging.getLogger("scope3ai.lib")
4✔
21

22

23
def init_anthropic_instrumentor() -> None:
4✔
24
    if importlib.util.find_spec("anthropic") is not None:
4✔
25
        from scope3ai.tracers.anthropic.instrument import AnthropicInstrumentor
4✔
26

27
        instrumentor = AnthropicInstrumentor()
4✔
28
        instrumentor.instrument()
4✔
29

30

31
def init_cohere_instrumentor() -> None:
4✔
32
    if importlib.util.find_spec("cohere") is not None:
4✔
33
        from scope3ai.tracers.cohere.instrument import CohereInstrumentor
4✔
34

35
        instrumentor = CohereInstrumentor()
4✔
36
        instrumentor.instrument()
4✔
37

38

39
def init_openai_instrumentor() -> None:
4✔
40
    if importlib.util.find_spec("openai") is not None:
4✔
41
        from scope3ai.tracers.openai.instrument import OpenAIInstrumentor
4✔
42

43
        instrumentor = OpenAIInstrumentor()
4✔
44
        instrumentor.instrument()
4✔
45

46

47
def init_huggingface_hub_instrumentor() -> None:
4✔
48
    if importlib.util.find_spec("huggingface_hub") is not None:
4✔
49
        from scope3ai.tracers.huggingface.instrument import HuggingfaceInstrumentor
4✔
50

51
        instrumentor = HuggingfaceInstrumentor()
4✔
52
        instrumentor.instrument()
4✔
53

54

55
def init_litellm_instrumentor() -> None:
4✔
56
    if importlib.util.find_spec("litellm") is not None:
4✔
57
        from scope3ai.tracers.litellm.instrument import LiteLLMInstrumentor
4✔
58

59
        instrumentor = LiteLLMInstrumentor()
4✔
60
        instrumentor.instrument()
4✔
61

62

63
def init_mistral_v1_instrumentor() -> None:
4✔
64
    if importlib.util.find_spec("mistralai") is not None:
4✔
65
        from scope3ai.tracers.mistralai.instrument import MistralAIInstrumentor
4✔
66

67
        instrumentor = MistralAIInstrumentor()
4✔
68
        instrumentor.instrument()
4✔
69

70

71
def init_response_instrumentor() -> None:
4✔
72
    from scope3ai.response_interceptor.instrument import ResponseInterceptor
4✔
73

74
    instrumentor = ResponseInterceptor()
4✔
75
    instrumentor.instrument()
4✔
76

77

78
_INSTRUMENTS = {
4✔
79
    PROVIDERS.ANTROPIC.value: init_anthropic_instrumentor,
80
    PROVIDERS.COHERE.value: init_cohere_instrumentor,
81
    PROVIDERS.OPENAI.value: init_openai_instrumentor,
82
    PROVIDERS.HUGGINGFACE_HUB.value: init_huggingface_hub_instrumentor,
83
    PROVIDERS.LITELLM.value: init_litellm_instrumentor,
84
    PROVIDERS.MISTRALAI.value: init_mistral_v1_instrumentor,
85
    PROVIDERS.RESPONSE.value: init_response_instrumentor,
86
}
87

88
_RE_INIT_PROVIDERS = [PROVIDERS.RESPONSE.value]
4✔
89

90

91
def generate_id() -> str:
4✔
UNCOV
92
    return uuid4().hex
×
93

94

95
class Scope3AIError(Exception):
4✔
96
    pass
4✔
97

98

99
class Scope3AI:
4✔
100
    """
101
    Scope3AI tracer class
102

103
    This class is a singleton that provides a context manager for tracing
104
    inference metadata and submitting impact requests to the Scope3 AI API.
105
    """
106

107
    _instance: Optional["Scope3AI"] = None
4✔
108
    _tracer: ContextVar[List[Tracer]] = ContextVar("tracer", default=[])
4✔
109
    _worker: Optional[BackgroundWorker] = None
4✔
110
    _providers: List[str] = []
4✔
111

112
    def __new__(cls, *args, **kwargs):
4✔
113
        if cls._instance is None:
4✔
114
            cls._instance = super(Scope3AI, cls).__new__(cls)
4✔
115
        return cls._instance
4✔
116

117
    @classmethod
4✔
118
    def init(
4✔
119
        cls,
120
        api_key: str = None,
121
        api_url: str = None,
122
        sync_mode: bool = False,
123
        enable_debug_logging: bool = False,
124
        providers: Optional[List[str]] = None,
125
    ) -> None:
126
        if cls._instance is not None:
4✔
UNCOV
127
            raise Scope3AIError("Scope3AI is already initialized")
×
128
        cls._instance = self = Scope3AI()
4✔
129
        self.api_key = api_key or getenv("SCOPE3AI_API_KEY")
4✔
130
        self.api_url = api_url or getenv("SCOPE3AI_API_URL") or DEFAULT_API_URL
4✔
131
        self.sync_mode = sync_mode or bool(getenv("SCOPE3AI_SYNC_MODE", False))
4✔
132
        if not self.api_key:
4✔
UNCOV
133
            raise Scope3AIError(
×
134
                "The scope3 api_key option must be set either by "
135
                "passing the API key to the Scope3AI.init(api_key='xxx') "
136
                "or by setting the SCOPE3AI_API_KEY environment variable"
137
            )
138
        if not self.api_url:
4✔
UNCOV
139
            raise Scope3AIError(
×
140
                "The api_url option must be set either by "
141
                "passing the API URL to the Scope3AI.init(api_url='xxx') "
142
                "or by setting the SCOPE3AI_API_URL environment variable"
143
            )
144

145
        if enable_debug_logging:
4✔
146
            self._init_logging()
4✔
147

148
        if providers is None:
4✔
149
            providers = list(_INSTRUMENTS.keys())
4✔
150

151
        http_client_options = {"api_key": self.api_key, "api_url": self.api_url}
4✔
152
        self._sync_client = Client(**http_client_options)
4✔
153
        self._async_client = AsyncClient(**http_client_options)
4✔
154
        self._init_providers(providers)
4✔
155
        self._init_atexit()
4✔
156
        return cls._instance
4✔
157

158
    @classmethod
4✔
159
    def get_instance(cls) -> "Scope3AI":
4✔
160
        """
161
        Return the instance of the Scope3AI singleton.
162

163
        This method provides access to the default global state of the
164
        Scope3AI library. The returned instance can be used to trace
165
        inference metadata and submit impact requests to the Scope3 AI
166
        API from anywhere in the application.
167

168
        Returns:
169
            Scope3AI: The singleton instance of the Scope3AI class.
170
        """
171
        return cls._instance
4✔
172

173
    def submit_impact(
4✔
174
        self,
175
        impact_row: ImpactRow,
176
    ) -> Scope3AIContext:
177
        """
178
        Submit an impact request to the Scope3 AI API.
179

180
        This function sends an impact request represented by the `impact_row`
181
        to the Scope3 AI API and optionally returns the response.
182

183
        Args:
184
            impact_row (ImpactRow): The impact request data
185
                that needs to be submitted to the Scope3 AI API.
186

187
        Returns:
188
            Scope3AIContext: A context object containing the request data and
189
            the response from the API.
190
        """
191

192
        def submit_impact(
4✔
193
            impact_row: ImpactRow,
194
            ctx: Scope3AIContext,
195
        ) -> Optional[ImpactResponse]:
196
            response = self._sync_client.impact(
4✔
197
                rows=[impact_row],
198
                with_response=True,
199
            )
200
            ctx.set_impact(response.rows[0])
4✔
201
            if ctx._tracer:
4✔
202
                ctx._tracer.add_impact(response.rows[0])
4✔
203
                ctx._tracer._unlink_trace(ctx)
4✔
204
            return response
4✔
205

206
        tracer = self.current_tracer
4✔
207
        ctx = Scope3AIContext(request=impact_row)
4✔
208
        ctx._tracer = tracer
4✔
209
        if tracer:
4✔
210
            tracer._link_trace(ctx)
4✔
211

212
        if self.sync_mode:
4✔
213
            submit_impact(impact_row, ctx=ctx)
4✔
214
            return ctx
4✔
215

216
        self._ensure_worker()
4✔
217
        self._worker.submit(partial(submit_impact, impact_row=impact_row, ctx=ctx))
4✔
218
        return ctx
4✔
219

220
    async def asubmit_impact(
4✔
221
        self,
222
        impact_row: ImpactRow,
223
    ) -> Scope3AIContext:
224
        """
225
        Async version of Scope3AI::submit_impact.
226
        """
227

228
        if not self.sync_mode:
4✔
229
            # in non sync-mode, it uses the background worker,
230
            # and the background worker is not async (does not have to be).
231
            # so we just redirect the call to the sync version.
232
            return self.submit_impact(impact_row)
4✔
233
        tracer = self.current_tracer
4✔
234
        ctx = Scope3AIContext(request=impact_row)
4✔
235
        ctx._tracer = tracer
4✔
236
        if tracer:
4✔
UNCOV
237
            tracer._link_trace(ctx)
×
238

239
        response = await self._async_client.impact(
4✔
240
            rows=[impact_row],
241
            with_response=True,
242
        )
243
        ctx.set_impact(response.rows[0])
4✔
244
        if tracer:
4✔
UNCOV
245
            tracer.add_impact(response.rows[0])
×
UNCOV
246
            tracer._unlink_trace(ctx)
×
247

248
        return ctx
4✔
249

250
    @property
4✔
251
    def root_tracer(self):
4✔
252
        """
253
        Return the root tracer.
254

255
        The root tracer is the first tracer in the current execution context
256
        (tracer stack). If no tracers are currently active, it returns None.
257

258
        Returns:
259
            Tracer: The root tracer if available, otherwise None.
260
        """
UNCOV
261
        tracers = self._tracer.get()
×
UNCOV
262
        return tracers[0] if tracers else None
×
263

264
    @property
4✔
265
    def current_tracer(self):
4✔
266
        """
267
        Return the current tracer.
268

269
        The current tracer is the last tracer in the current execution context
270
        (tracer stack). If no tracers are currently active, it returns None.
271

272
        Returns:
273
            Tracer: The current tracer if available, otherwise None.
274
        """
275
        tracers = self._tracer.get()
4✔
276
        return tracers[-1] if tracers else None
4✔
277

278
    @contextmanager
4✔
279
    def trace(self):
4✔
280
        tracer = Tracer()
4✔
281
        try:
4✔
282
            self._push_tracer(tracer)
4✔
283
            yield tracer
4✔
284
        finally:
285
            self._pop_tracer(tracer)
4✔
286

287
    def close(self):
4✔
288
        if self._worker:
4✔
289
            self._worker.kill()
4✔
290
        self.__class__._instance = None
4✔
291

292
    #
293
    # Internals
294
    #
295

296
    def _push_tracer(self, tracer: Tracer) -> None:
4✔
297
        tracer._link_parent(self.current_tracer)
4✔
298
        self._tracer.get().append(tracer)
4✔
299

300
    def _pop_tracer(self, tracer: Tracer) -> None:
4✔
301
        self._tracer.get().remove(tracer)
4✔
302
        tracer._unlink_parent(self.current_tracer)
4✔
303

304
    def _init_providers(self, providers: List[str]) -> None:
4✔
305
        """Initialize the specified providers."""
306
        for provider in providers:
4✔
307
            if provider not in _INSTRUMENTS:
4✔
UNCOV
308
                raise Scope3AIError(
×
309
                    f"Could not find tracer for the `{provider}` provider."
310
                )
311
            if provider in self._providers and provider not in _RE_INIT_PROVIDERS:
4✔
312
                # already initialized
313
                continue
4✔
314
            init_func = _INSTRUMENTS[provider]
4✔
315
            init_func()
4✔
316
            self._providers.append(provider)
4✔
317

318
    def _ensure_worker(self) -> None:
4✔
319
        if not self._worker:
4✔
320
            self._worker = BackgroundWorker(-1)
4✔
321

322
    def _init_logging(self) -> None:
4✔
323
        logging.basicConfig(
4✔
324
            level=logging.INFO,
325
            format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
326
            handlers=[logging.StreamHandler()],
327
        )
328
        logging.getLogger("scope3ai").setLevel(logging.DEBUG)
4✔
329

330
    def _init_atexit(self):
4✔
331
        @atexit.register
4✔
332
        def _shutdown():
4✔
333
            # do not reinstanciate the singleton here if somehow it was deleted
UNCOV
334
            scope3ai = Scope3AI._instance
×
UNCOV
335
            if not scope3ai:
×
UNCOV
336
                return
×
UNCOV
337
            if scope3ai._worker and scope3ai._worker._queue:
×
UNCOV
338
                logging.debug("Waiting background informations to be processed")
×
UNCOV
339
                scope3ai._worker._queue.join()
×
UNCOV
340
                logging.debug("Shutting down Scope3AI")
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc