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

scope3data / scope3ai-py / 12877259899

20 Jan 2025 11:36PM UTC coverage: 95.839% (+15.3%) from 80.557%
12877259899

Pull #71

github

a32a0d
tito
fix: use > 0 for request duration
Pull Request #71: fix(huggingface): fix image classification and segmentation to support Path

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

43 existing lines in 9 files now uncovered.

2119 of 2211 relevant lines covered (95.84%)

3.83 hits per line

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

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

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

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

21

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

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

29

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

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

37

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

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

45

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

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

53

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

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

61

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

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

69

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

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

76

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

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

89

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

93

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

97

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

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

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

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

116
    def __init__(self):
4✔
117
        self.api_key: Optional[str] = None
4✔
118
        self.api_url: Optional[str] = None
4✔
119
        self.sync_mode: bool = False
4✔
120
        self._sync_client: Optional[Client] = None
4✔
121
        self._async_client: Optional[AsyncClient] = None
4✔
122

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

151
        if enable_debug_logging:
4✔
152
            self._init_logging()
4✔
153

154
        if providers is None:
4✔
155
            providers = list(_INSTRUMENTS.keys())
4✔
156

157
        http_client_options = {"api_key": self.api_key, "api_url": self.api_url}
4✔
158
        self._sync_client = Client(**http_client_options)
4✔
159
        self._async_client = AsyncClient(**http_client_options)
4✔
160
        self._init_providers(providers)
4✔
161
        self._init_atexit()
4✔
162
        return cls._instance
4✔
163

164
    @classmethod
4✔
165
    def get_instance(cls) -> "Scope3AI":
4✔
166
        """
167
        Return the instance of the Scope3AI singleton.
168

169
        This method provides access to the default global state of the
170
        Scope3AI library. The returned instance can be used to trace
171
        inference metadata and submit impact requests to the Scope3 AI
172
        API from anywhere in the application.
173

174
        Returns:
175
            Scope3AI: The singleton instance of the Scope3AI class.
176
        """
177
        return cls._instance
4✔
178

179
    def submit_impact(
4✔
180
        self,
181
        impact_row: ImpactRow,
182
    ) -> Scope3AIContext:
183
        """
184
        Submit an impact request to the Scope3 AI API.
185

186
        This function sends an impact request represented by the `impact_row`
187
        to the Scope3 AI API and optionally returns the response.
188

189
        Args:
190
            impact_row (ImpactRow): The impact request data
191
                that needs to be submitted to the Scope3 AI API.
192

193
        Returns:
194
            Scope3AIContext: A context object containing the request data and
195
            the response from the API.
196
        """
197

198
        def submit_impact(
4✔
199
            impact_row: ImpactRow,
200
            ctx: Scope3AIContext,
201
        ) -> Optional[ImpactResponse]:
202
            response = self._sync_client.impact(
4✔
203
                rows=[impact_row],
204
                with_response=True,
205
            )
206
            ctx.set_impact(response.rows[0])
4✔
207
            if ctx._tracer:
4✔
208
                ctx._tracer.add_impact(response.rows[0])
4✔
209
                ctx._tracer._unlink_trace(ctx)
4✔
210
            return response
4✔
211

212
        tracer = self.current_tracer
4✔
213
        ctx = Scope3AIContext(request=impact_row)
4✔
214
        ctx._tracer = tracer
4✔
215
        if tracer:
4✔
216
            tracer._link_trace(ctx)
4✔
217

218
        if self.sync_mode:
4✔
219
            submit_impact(impact_row, ctx=ctx)
4✔
220
            return ctx
4✔
221

222
        self._ensure_worker()
4✔
223
        self._worker.submit(partial(submit_impact, impact_row=impact_row, ctx=ctx))
4✔
224
        return ctx
4✔
225

226
    async def asubmit_impact(
4✔
227
        self,
228
        impact_row: ImpactRow,
229
    ) -> Scope3AIContext:
230
        """
231
        Async version of Scope3AI::submit_impact.
232
        """
233

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

245
        response = await self._async_client.impact(
4✔
246
            rows=[impact_row],
247
            with_response=True,
248
        )
249
        ctx.set_impact(response.rows[0])
4✔
250
        if tracer:
4✔
UNCOV
251
            tracer.add_impact(response.rows[0])
×
UNCOV
252
            tracer._unlink_trace(ctx)
×
253

254
        return ctx
4✔
255

256
    @property
4✔
257
    def root_tracer(self):
4✔
258
        """
259
        Return the root tracer.
260

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

264
        Returns:
265
            Tracer: The root tracer if available, otherwise None.
266
        """
267
        tracers = self._tracer.get()
×
268
        return tracers[0] if tracers else None
×
269

270
    @property
4✔
271
    def current_tracer(self):
4✔
272
        """
273
        Return the current tracer.
274

275
        The current tracer is the last tracer in the current execution context
276
        (tracer stack). If no tracers are currently active, it returns None.
277

278
        Returns:
279
            Tracer: The current tracer if available, otherwise None.
280
        """
281
        tracers = self._tracer.get()
4✔
282
        return tracers[-1] if tracers else None
4✔
283

284
    @contextmanager
4✔
285
    def trace(self):
4✔
286
        tracer = Tracer()
4✔
287
        try:
4✔
288
            self._push_tracer(tracer)
4✔
289
            yield tracer
4✔
290
        finally:
291
            self._pop_tracer(tracer)
4✔
292

293
    def close(self):
4✔
294
        if self._worker:
4✔
295
            self._worker.kill()
4✔
296
        self.__class__._instance = None
4✔
297

298
    #
299
    # Internals
300
    #
301

302
    def _push_tracer(self, tracer: Tracer) -> None:
4✔
303
        tracer._link_parent(self.current_tracer)
4✔
304
        self._tracer.get().append(tracer)
4✔
305

306
    def _pop_tracer(self, tracer: Tracer) -> None:
4✔
307
        self._tracer.get().remove(tracer)
4✔
308
        tracer._unlink_parent(self.current_tracer)
4✔
309

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

324
    def _ensure_worker(self) -> None:
4✔
325
        if not self._worker:
4✔
326
            self._worker = BackgroundWorker(-1)
4✔
327

328
    def _init_logging(self) -> None:
4✔
329
        logging.basicConfig(
4✔
330
            level=logging.INFO,
331
            format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
332
            handlers=[logging.StreamHandler()],
333
        )
334
        logging.getLogger("scope3ai").setLevel(logging.DEBUG)
4✔
335

336
    def _init_atexit(self):
4✔
337
        @atexit.register
4✔
338
        def _shutdown():
4✔
339
            # do not reinstanciate the singleton here if somehow it was deleted
UNCOV
340
            scope3ai = Scope3AI._instance
×
UNCOV
341
            if not scope3ai:
×
UNCOV
342
                return
×
UNCOV
343
            if scope3ai._worker and scope3ai._worker._queue:
×
UNCOV
344
                logging.debug("Waiting background informations to be processed")
×
UNCOV
345
                scope3ai._worker._queue.join()
×
UNCOV
346
                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