tensicai — the official Python SDK for tensic.ai
Talk to a tensic.ai server from your own product: chat with RAG, agent
and router projects, stream answers, keep conversations, manage the knowledge base, mint
scoped API keys, and use the OpenAI-compatible endpoints — from a sync (Tensic) or async
(AsyncTensic) client with full type hints and two runtime dependencies (httpx and
anyio, the async layer httpx.AsyncClient itself runs on).
- Install
- 60-second example
- Streaming
- Conversations
- RAG knowledge
- Attachments and vision
- Projects and API keys
- Async
- Errors and retries
- Configuration
- OpenAI-compatible direct access
- Widget client
- More namespaces
- Server compatibility
- Development
- Releasing
- License
Install
pip install tensicai
Requires Python 3.10+ and a tensic.ai server (self-hosted, or a hosted instance from tensic.ai). Create an API key in the tensic.ai UI (Users → API keys) and export it:
export TENSIC_URL="https://tensic.example.com" # your server
export TENSIC_API_KEY="sk-..." # a user or project-scoped key
60-second example
from tensicai import Tensic
client = Tensic() # reads TENSIC_URL and TENSIC_API_KEY from the environment
response = client.chat.send(project_id=1, question="What is our refund policy?")
print(response.answer)
for source in response.sources:
print(f"- {source.source} (score {source.score:.2f})")
tokens = response.tokens.total if response.tokens else 0
print(f"{tokens} tokens, conversation id {response.id}")
chat.send() runs one turn against a project. Every response is a lenient dataclass
(ChatResponse) — unknown fields never break parsing (response.extra, response.raw).
Pass chat_id= to continue an existing conversation, or use Conversations.
Streaming
Streaming yields typed events as the server produces them. stream.text() gives just the
answer deltas; iterating the stream gives every event (plans, steps, tool calls, warnings,
the final response).
from tensicai import Tensic
from tensicai.types import TextEvent, ToolCallStartedEvent
client = Tensic()
with client.chat.stream(project_id=1, question="Summarise this week's tickets") as stream:
for event in stream:
if isinstance(event, TextEvent):
print(event.text, end="", flush=True)
elif isinstance(event, ToolCallStartedEvent):
print(f"\n[calling {event.tool}]")
print()
final = stream.response # the authoritative ChatResponse, or None if the stream broke early
print("answer:", stream.answer)
print("chat id:", stream.chat_id, "| last event id:", stream.last_event_id)
Or just the text:
from tensicai import Tensic
client = Tensic()
for delta in client.chat.stream(project_id=1, question="Tell me a story").text():
print(delta, end="")
Details worth knowing:
- Event classes live in
tensicai.types:TextEvent,PlanEvent,StepStartEvent,StepDoneEvent,ToolCallStartedEvent,ToolCallCompletedEvent,WarningEvent,ErrorEvent,StoppedEvent,ResponseEvent,UnknownEvent. All carry.type,.rawand.event_id. - If the server emits an error frame, the stream yields the
ErrorEventand then raisesChatStreamError(.code,.message,.chat_id,.last_event_id). Passraise_on_error=Falseto inspectstream.erroryourself instead. - Resume an interrupted turn by re-sending the same request with
last_event_id=stream.last_event_idand the samechat_id; the server replays what you missed and tails the live run. - Cancel an in-flight turn from anywhere with
client.chat.stop(project_id, chat_id). - Events are buffered, so
stream.answer,stream.responseandstream.events()keep working after iteration ends.
Conversations
A Conversation remembers the project and chat id for you (a uuid4 hex id is generated when
you do not pass one) and collects every ChatResponse in .history:
from tensicai import Tensic
client = Tensic()
conversation = client.chat.conversation(project_id=1) # or chat_id="support-42"
first = conversation.send("Which plans include SSO?")
follow_up = conversation.send("And how much does the cheapest one cost?")
print(conversation.id, len(conversation.history), "turns")
print(follow_up.answer)
for delta in conversation.stream("Thanks — one-line summary?").text():
print(delta, end="")
conversation.stop() # cancels an in-flight turn, no-op otherwise
RAG knowledge
RAG projects own a knowledge base. Ingest text, URLs or files, search it, and manage sources:
from tensicai import Tensic
client = Tensic()
project_id = 1
result = client.knowledge.ingest_text(
project_id,
"Refunds are accepted within 30 days of purchase.",
source="policies/refunds.md",
keywords=["refund", "policy"],
)
print(f"{result.chunks} chunks from {result.documents} document(s)")
client.knowledge.ingest_url(project_id, "https://example.com/handbook")
client.knowledge.ingest_file(project_id, "handbook.pdf", method="auto", chunks=512)
for hit in client.knowledge.search(project_id, "refund window", k=5, score=0.2):
print(hit.source, hit.score, hit.id)
print(client.knowledge.list_sources(project_id))
chunks = client.knowledge.get_source(project_id, "policies/refunds.md")
print(len(chunks.ids), "chunks stored")
client.knowledge.delete_source(project_id, "policies/refunds.md")
Large batches go through knowledge.ingest_bulk(project_id, [paths...]) which queues jobs
server-side (knowledge.bulk_jobs() / cancel_bulk_job()), and knowledge.reembed() /
reembed_status() rebuild the vectors after changing the embedding model. ingest_file
accepts a path, bytes, an open binary file, or a (filename, content[, content_type])
tuple. Source names are sent to the server base64-encoded exactly like the web UI does, so any
string is a valid source name.
Attachments and vision
Send images to vision-capable models and files to agents. Files can be sent inline (base64) or uploaded first, which is better for anything larger than a few hundred KB:
from tensicai import Tensic
client = Tensic()
# Vision: an http(s)/data URL, a base64 string, raw bytes or a pathlib.Path.
reply = client.chat.send(1, "What is in this picture?", image="https://example.com/cat.jpg")
print(reply.answer)
# Inline files (max 10 per turn): paths, bytes, open files, tuples or ready-made dicts.
reply = client.chat.send(1, "Summarise the attached report", files=["q3-report.pdf"])
# Upload once, reference many times (needs object storage configured on the server).
attachment = client.chat.upload_attachment(1, "q3-report.pdf")
reply = client.chat.send(1, "List the action items", files=[attachment])
print(attachment.upload_id, attachment.expires_at)
# Files the agent wrote to /artifacts/ come back with a 24h asset token; fetch the bytes:
for artifact in reply.artifacts:
if artifact.token:
data = client.chat.get_asset(1, artifact.token, download=True)
print(artifact.name, artifact.mime_type, len(data), "bytes")
# reply.image (vision projects) is the model's image as a base64 data URL, not a token.
if reply.image:
print(reply.image[:30], "...")
image= is never a filesystem path. A str is the base64 payload itself or an
http(s):// / data: URL (the server fetches an http(s) one for you); raw bytes and a
pathlib.Path are read and base64-encoded by the SDK. So a local picture is
image=Path("cat.png") or image=open("cat.png", "rb").read() — and passing the filename as a
str raises ValueError before any request, pointing you at Path(...), rather than silently
sending the model a filename it can never see. files= is the opposite: there a str is a
path.
client.image.generate(...) takes the same values for its image-to-image image=, minus the
http(s) URL: that endpoint base64-decodes whatever it is given instead of fetching, so pass
base64, a data: URL, bytes or a Path.
Projects and API keys
from tensicai import Tensic
client = Tensic()
# Browse (auto-paging) and look up projects
for project in client.projects.iter():
print(project.id, project.name, project.type, project.llm)
support = client.projects.find("support-bot")
# Create a RAG project and tune it (PATCH then GET — you always get the full object back)
project = client.projects.create(
"support-bot",
type="rag",
team_id=1,
llm="gpt-4o",
embeddings="text-embedding-3-small",
human_name="Support bot",
)
project_id = project.id
assert project_id is not None # models are lenient, so ids are Optional to the type checker
project = client.projects.update(project_id, system="You are a concise support assistant.", k=6)
# Mint a read-only API key scoped to this project for your integration
key = client.users.api_keys.create(
"integration-user",
team_id=1,
description="website chat",
allowed_projects=[project_id],
read_only=True,
)
print(key.api_key) # shown once — store it now
Sub-resources hang off client.projects and take the project id first: prompts, secrets,
routines, widgets, logs, conversations, analytics, evals, memory, comments,
guards, custom_tools, webhooks, integrations. For example
client.projects.widgets.create(project.id, name="Site chat", allowed_domains=["example.com"])
returns the widget key (current servers return the live key from every widget read too, so
treat those payloads as secret-bearing), and client.projects.logs.list(project.id, has_error=True) lists
failed turns.
projects.update(options=...) replaces the whole options blob server-side (the server fills
defaults for keys you omit and only preserves sensitive ones), so read, modify, write:
from tensicai import Tensic
client = Tensic()
project = client.projects.get(1)
options = dict(project.options)
options["max_iterations"] = 8
project = client.projects.update(1, options=options)
print(project.options)
The same read-modify-write applies to every other option blob the API exposes:
teams.update(options=/branding=), users.update(options=), llms.update(options=),
embeddings.update(options=) and the image-generator / speech-to-text registries all
replace what they are given rather than merging it key by key (masked "********"
credentials are the one thing carried forward). classifiers.update(options=) is the single
exception — that endpoint does merge.
Team membership and grants are addressed by name: teams.create(...) / teams.update(...)
take usernames, project names and model names in users=, admins=, projects=, llms=,
embeddings=; users.update(username, projects=[...]) likewise takes project names.
Async
AsyncTensic mirrors Tensic method for method; streaming methods are awaited and return an
AsyncChatStream:
import asyncio
from tensicai import AsyncTensic
async def main() -> None:
async with AsyncTensic() as client:
me = await client.whoami()
print("hello,", me.username)
reply = await client.chat.send(project_id=1, question="Ping?")
print(reply.answer)
stream = await client.chat.stream(project_id=1, question="Stream me")
async for delta in stream.text():
print(delta, end="")
async for project in client.projects.iter():
print(project.name)
asyncio.run(main())
Errors and retries
Every HTTP failure raises a subclass of tensicai.APIStatusError (itself a TensicError)
carrying status_code, the server's stable code, message, fields (validation details),
request_id, retry_after, rate_limit, body, headers and the raw response.
| Exception | Status | Typical code |
|---|---|---|
BadRequestError |
400 | invalid_request |
AuthenticationError |
401 | unauthenticated |
BudgetExceededError |
402 | budget_exceeded |
PermissionDeniedError |
403 | forbidden (e.g. read-only key) |
NotFoundError |
404 | not_found |
ConflictError |
409 | conflict |
PayloadTooLargeError |
413 | payload_too_large |
UnsupportedMediaTypeError |
415 | unsupported_media_type |
ValidationError |
422 | validation_error (see .fields) |
RateLimitError |
429 | rate_limited, quota_exceeded |
InternalServerError |
5xx | internal |
BadGatewayError |
502 | bad_gateway |
ServiceUnavailableError |
503 | service_unavailable |
APIConnectionError |
— | network failure |
APITimeoutError |
— | timeout (subclass of the above) |
TwoFactorRequiredError |
— | Tensic.login() needs a TOTP code |
ChatStreamError |
— | error frame mid-stream |
UnsupportedOperationError |
— | the SDK cannot do this locally |
from tensicai import NotFoundError, RateLimitError, Tensic, ValidationError
client = Tensic()
try:
client.chat.send(project_id=999, question="hi")
except NotFoundError as err:
print(err.status_code, err.code, err.message, err.request_id)
except ValidationError as err:
print(err.fields) # {"body.question": "field required", ...}
except RateLimitError as err:
print("retry in", err.retry_after, "s;", err.rate_limit)
Retries: transient failures (408, 429, 502, 503, 504 and connection errors) are retried up to
max_retries times (default 2) with exponential back-off and jitter, honouring Retry-After.
Only idempotent requests are retried — GET/PUT/DELETE, read-like POSTs such as search and
classify, and POSTs carrying an Idempotency-Key. chat.send() generates one automatically
(pass idempotency_key= to control it), so a retried chat turn is never executed twice; a
409 "still in flight" reply for that key is retried too. Streaming requests are never retried
once the body has started. Disable retries with Tensic(max_retries=0) or per call site with
client.with_options(max_retries=0).
Configuration
| Setting | Constructor argument | Environment variable |
|---|---|---|
| Server URL | Tensic(base_url=...) |
TENSIC_URL (alias TENSIC_BASE_URL); default http://localhost:9000 |
| API key | Tensic(api_key=...) |
TENSIC_API_KEY |
| Timeout | timeout=60.0 or an httpx.Timeout |
— |
| Retries | max_retries=2 |
— |
| Extra headers | default_headers={...} |
— |
| TLS verification | verify=True / CA bundle path / SSLContext |
— |
| Debug logging | — | TENSIC_LOG=debug (logger tensicai, no secrets) |
import httpx
from tensicai import Tensic
# Explicit configuration
client = Tensic(
base_url="https://tensic.example.com",
api_key="sk-...",
timeout=httpx.Timeout(120.0, connect=5.0),
max_retries=3,
default_headers={"X-Team": "growth"},
)
# Per-call overrides without rebuilding the connection pool
patient = client.with_options(timeout=600.0, max_retries=0)
patient.knowledge.ingest_file(1, "big-manual.pdf")
# Long-running methods also accept timeout= directly
client.chat.send(1, "Deep research, please", timeout=300.0)
# Bring your own httpx client for proxies, custom transports or client certificates
http = httpx.Client(proxy="http://proxy.internal:3128", verify="/etc/ssl/corp-ca.pem")
client = Tensic("https://tensic.example.com", "sk-...", http_client=http)
# Clients are context managers; close() releases the pool
with Tensic() as scoped:
print(scoped.version().version)
Username/password login is available too (it keeps the server's session cookie on the client):
from tensicai import Tensic, TwoFactorRequiredError
try:
client = Tensic.login("alice", "s3cret", base_url="https://tensic.example.com")
except TwoFactorRequiredError:
client = Tensic.login("alice", "s3cret", base_url="https://tensic.example.com", totp_code="123456")
print(client.whoami().username)
client.logout()
Instances with LDAP authentication enabled keep the credentials in the directory, so those
accounts have no local password for HTTP Basic to verify and login() cannot sign them in.
Use ldap_login(), which posts the credentials to /ldap instead:
from tensicai import Tensic
client = Tensic.ldap_login("alice", "s3cret", base_url="https://tensic.example.com")
print(client.whoami().username)
client.logout()
It yields the same tensic_token cookie session as login() (and AsyncTensic.ldap_login()
is the async twin), with no TOTP step on this route. The account the session belongs to is
named after the directory's mail attribute, so whoami().username is usually the address
rather than the login name you passed. LDAP being disabled server-side, an empty password, a
user missing from the directory and a failed bind all come back as BadRequestError.
Streaming requests use httpx.Timeout(None, connect=10.0) (no read timeout) unless you pass
timeout=. The default timeout for everything else is 60 s with a 10 s connect timeout.
OpenAI-compatible direct access
The server exposes the OpenAI API surface under /v1 — no project needed, model is the
tensic LLM name. Use the SDK:
from tensicai import Tensic
client = Tensic()
completion = client.direct.chat_completions(
"gpt-4o",
[{"role": "user", "content": "Say hello in Portuguese"}],
temperature=0.2,
)
print(completion.content)
stream = client.direct.chat_completions("gpt-4o", [{"role": "user", "content": "Count to 5"}], stream=True)
for chunk in stream:
print(chunk.content or "", end="")
print("\n", stream.content) # the joined text
vectors = client.direct.embeddings("text-embedding-3-small", ["hello", "world"]).vectors
print(len(vectors), "vectors of", len(vectors[0]), "dimensions")
print([model.id for model in client.direct.models()])
image = client.direct.images_generate("a lighthouse at dawn", model="dall-e-3", size="1024x1024")
transcript = client.direct.audio_transcribe("meeting.mp3", model="whisper-1", language="en")
project_id= on chat_completions() / models() uses the project-governed variants
(/projects/{id}/v1/...), which apply the project's guard and budget. raise_on_error=False
on a streamed completion exposes stream.error instead of raising.
Or point the official openai package at the server:
from openai import OpenAI
from tensicai import Tensic
tensic = Tensic()
openai = OpenAI(base_url=tensic.openai_base_url, api_key=tensic.api_key)
print(openai.chat.completions.create(
model="gpt-4o", messages=[{"role": "user", "content": "Hello!"}]
).choices[0].message.content)
Widget client
Published chat widgets authenticate with a widget key (wk_...) instead of a user key —
the same thing the embeddable JavaScript does. WidgetClient reads TENSIC_URL and
TENSIC_WIDGET_KEY when arguments are omitted.
from tensicai import WidgetClient
widget = WidgetClient("https://tensic.example.com", "wk_...")
print(widget.config().title)
reply = widget.chat("Do you ship to Portugal?")
print(reply.answer)
reply = widget.chat("How long does it take?", chat_id=reply.id) # continue the conversation
for delta in widget.stream("And the cost?", chat_id=reply.id).text():
print(delta, end="")
AsyncWidgetClient is the async twin. Pass context_token= to send a signed
X-Widget-Context token.
More namespaces
Everything else the server offers is one attribute away; every method has a docstring with the endpoint it calls.
| Namespace | What it covers |
|---|---|
client.teams |
list/iter/get/create/update/delete, add/remove users, admins and projects, grant/revoke models, analytics, transactions, branding, member budgets, invites |
client.users |
CRUD, team budgets, TOTP setup/enable/disable/status, client.users.api_keys |
client.llms, client.embeddings |
model registry CRUD (+ llms.test) |
client.tools |
classify(sequence, labels) zero-shot classification, agent/MCP tool discovery, Ollama and OpenAI-compatible probes |
client.statistics |
platform summary, daily tokens, top LLMs/projects, per-user usage |
client.templates |
list/get/update/delete and instantiate(template_id, name, team_id=...) |
client.image, client.audio |
generators/STT models available to you, generate(...), transcribe(...) |
client.settings |
admin settings (update(values=None, **fields)), infrastructure, health, audit, cron logs, test_* connectivity checks |
client.classifiers, client.image_generators, client.speech_to_text |
registry CRUD |
client.invitations, client.examples, client.news |
invitations, example projects, platform news |
client.search(query), client.whoami(), client.version(), client.health() / client.health(probe="live") |
global search, current user, server version, readiness / liveness probe (GET /health/ready, /health/live) |
Anything not wrapped is reachable through the escape hatches client.get(path, params=...),
client.post(path, json=...), client.patch, client.put, client.delete, which return the
decoded body and raise the same typed errors.
Writing your own typed wrapper? The aliases the SDK's own signatures use are public too:
from tensicai import NOT_GIVEN, NotGiven, FileInput, AttachmentInput, Timeout and
from tensicai.types import ChatResponse, Privacy, Splitter, TemplateVisibility.
Not wrapped
A few routes are deliberately left to the escape hatch, because a typed method would not help anyone:
| Endpoint | Why |
|---|---|
GET /oauth/{provider}/login, GET /oauth/{provider}/callback |
a browser redirect dance — the SDK never follows redirects, and the callback is called by the identity provider, not by you |
GET/POST /webhooks/whatsapp |
inbound: Meta calls your server on it (verification handshake, then message delivery) |
POST /mcp |
MCP JSON-RPC, which an MCP client (Claude, an IDE, the mcp package) speaks directly — point it at the URL and let it handle the protocol |
POST /auth/support-login |
an operator-only exchange of a manager-signed, ~90-second, single-use grant for an admin session; not a customer integration path |
If you do need one of them, the escape hatch is one line — same auth, same decoded body, same typed errors:
from tensicai import Tensic
client = Tensic()
tools = client.post("/mcp", json={"jsonrpc": "2.0", "id": 1, "method": "tools/list"})
print(tools)
Project-scoped webhooks — the outbound ones your project calls — are wrapped:
client.projects.webhooks.
Server compatibility
tensicai 0.1.x targets tensic.ai server 1.6 or newer. Models are lenient: fields added by
newer servers land in .extra, and missing ones read as None, so a newer server never breaks
an older SDK. Check what you are talking to with client.version().
Development
git clone https://github.com/tensicai/python-sdk && cd python-sdk
make install # uv sync --extra dev
make check # ruff check + ruff format --check + mypy src examples + pytest
make test-all # pytest on Python 3.10 through 3.14 (uv fetches interpreters)
Tests never touch the network (they run against an in-memory httpx.MockTransport).
Live tests in tests/integration/ run only when TENSIC_URL and TENSIC_API_KEY are set.
See CONTRIBUTING.md for the
full workflow and docs/INTERNALS.md
for how the SDK is put together.
Releasing
- Bump
__version__insrc/tensicai/_version.pyand add the entry toCHANGELOG.md. - Commit, then tag and push:
git tag vX.Y.Z && git push origin main vX.Y.Z. - The
publish.ymlworkflow runs ruff, mypy and the full test suite, builds the sdist and wheel, checks that the tag matches the version, and publishes to PyPI through Trusted Publishing (no API token involved). A red suite fails the release before anything is uploaded.
One-time PyPI setup: add a pending Trusted Publisher on https://pypi.org/manage/account/publishing/
for project tensicai, owner tensicai, repository python-sdk, workflow publish.yml,
environment pypi.
To rehearse a release, run the Publish workflow manually (Actions → Publish → Run
workflow) with the Publish to TestPyPI input enabled: manual runs build and verify the
package and upload it to TestPyPI only — production PyPI is reached exclusively by pushing a
vX.Y.Z tag that matches __version__. For that rehearsal, also register a pending Trusted
Publisher on https://test.pypi.org/manage/account/publishing/ with the same owner, repository
and workflow, but environment testpypi.
License
Apache-2.0 — see LICENSE.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file tensicai-0.1.0.tar.gz.
File metadata
- Download URL: tensicai-0.1.0.tar.gz
- Upload date:
- Size: 279.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1ab994d0b463e36d92e7b478b534ada496a3ea063bcf7bd799f18b846270c369
|
|
| MD5 |
487896d3a7836fae9600787b857cf961
|
|
| BLAKE2b-256 |
f2e1b85b104b950ad1fd1e6bb329e7ceb73392bb41768c468c02400039d53777
|
Provenance
The following attestation bundles were made for tensicai-0.1.0.tar.gz:
Publisher:
publish.yml on tensicai/python-sdk
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tensicai-0.1.0.tar.gz -
Subject digest:
1ab994d0b463e36d92e7b478b534ada496a3ea063bcf7bd799f18b846270c369 - Sigstore transparency entry: 2688362159
- Sigstore integration time:
-
Permalink:
tensicai/python-sdk@bbbf79ffdbc479c5408fb5912c16aff250e92218 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/tensicai
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@bbbf79ffdbc479c5408fb5912c16aff250e92218 -
Trigger Event:
push
-
Statement type:
File details
Details for the file tensicai-0.1.0-py3-none-any.whl.
File metadata
- Download URL: tensicai-0.1.0-py3-none-any.whl
- Upload date:
- Size: 172.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a44b0fc8239ab1c771e89cc73132c7813ead8c9228462344123f44c8795fe81f
|
|
| MD5 |
ee84d5584722bc0bf8ab1329b18a3d39
|
|
| BLAKE2b-256 |
89351ea95dcb6492989f110b4e306d5139a7b07f0464cf4b89c9779018fb1cb6
|
Provenance
The following attestation bundles were made for tensicai-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on tensicai/python-sdk
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tensicai-0.1.0-py3-none-any.whl -
Subject digest:
a44b0fc8239ab1c771e89cc73132c7813ead8c9228462344123f44c8795fe81f - Sigstore transparency entry: 2688362276
- Sigstore integration time:
-
Permalink:
tensicai/python-sdk@bbbf79ffdbc479c5408fb5912c16aff250e92218 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/tensicai
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@bbbf79ffdbc479c5408fb5912c16aff250e92218 -
Trigger Event:
push
-
Statement type: