From c3611c954cbbc20312d573b0c83876585e85a795 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Fri, 27 Mar 2026 13:29:29 +0000 Subject: [PATCH 01/60] feat: add shared group directories and NSS wrapper - Add shared-storage RWX PVC volume support with per-group subPaths mounted at /shared/ inside user pods - Add init container that creates group directories with chmod 2775 (setgid bit) so new files inherit the group and are group-writable - Add libnss_wrapper.so configuration so whoami/id report the real username instead of 'jovyan', with NB_UMASK=0002 - Refactor pre_spawn_hook into focused single-responsibility functions: _get_user_groups, _setup_shared_storage, _setup_nss_wrapper - Orchestrator _pre_spawn_hook chains Nebi auth, shared storage, and NSS wrapper; always registered (NSS runs even without shared storage) - Add sharedStorage.groups allowlist and mountPathPrefix values - Add jupyterhub.custom.shared-storage-* config keys --- config/jupyterhub/01-spawner.py | 148 +++++++++++++++++++++++++++++++- values.yaml | 10 +++ 2 files changed, 155 insertions(+), 3 deletions(-) diff --git a/config/jupyterhub/01-spawner.py b/config/jupyterhub/01-spawner.py index e89ca0f..e8f4e53 100644 --- a/config/jupyterhub/01-spawner.py +++ b/config/jupyterhub/01-spawner.py @@ -74,6 +74,23 @@ } +# --------------------------------------------------------------------------- +# Shared storage (RWX PVC for group directories) +# --------------------------------------------------------------------------- +# When enabled, mount the shared-storage PVC so users can collaborate via +# /shared/. Group-specific subPaths and init containers are added +# dynamically in _setup_shared_storage() at spawn time based on group membership. +shared_storage_enabled = get_config("custom.shared-storage-enabled", False) +shared_storage_groups_allowlist = get_config("custom.shared-storage-groups", []) +shared_storage_mount_prefix = get_config("custom.shared-storage-mount-prefix", "/shared") + +if shared_storage_enabled: + c.KubeSpawner.volumes.append({ + "name": "shared", + "persistentVolumeClaim": {"claimName": "shared-storage"}, + }) + + # --------------------------------------------------------------------------- # Nebi binary (init container) # --------------------------------------------------------------------------- @@ -516,6 +533,131 @@ async def _nebi_pre_spawn_hook(spawner): log.exception("Nebi auto-auth failed for %s (pod will still spawn)", spawner.user.name) -# Only register the hook when Nebi integration is configured. -if nebi_remote_url and get_config("custom.nebi-internal-url", ""): - c.KubeSpawner.pre_spawn_hook = _nebi_pre_spawn_hook +# --------------------------------------------------------------------------- +# Shared storage hook helpers +# --------------------------------------------------------------------------- + +def _get_user_groups(spawner, auth_state): + """Extract and filter user groups from auth_state. + + Returns groups from the Keycloak IdToken (set by EnvoyOIDCAuthenticator), + falling back to JupyterHub-managed groups. Applies the allowlist if configured. + """ + groups = [] + if auth_state: + groups = auth_state.get("groups", []) + if not groups: + groups = [g.name for g in spawner.user.groups] + if shared_storage_groups_allowlist: + groups = [g for g in groups if g in shared_storage_groups_allowlist] + # Strip any leading slashes (Keycloak returns e.g. "/admin") + return [g.strip("/") for g in groups if g] + + +async def _setup_shared_storage(spawner, groups): + """Add per-group volume mounts and a single init container for shared directories. + + Creates /shared/ on the PVC (chmod 2775 so new files inherit the + group and are group-writable with NB_UMASK=0002). + """ + for group in groups: + spawner.volume_mounts = list(spawner.volume_mounts) + [{ + "mountPath": f"{shared_storage_mount_prefix}/{group}", + "name": "shared", + "subPath": f"shared/{group}", + }] + + mkdir_cmds = " && ".join([ + f"mkdir -p /mnt/shared/{g} && chmod 2775 /mnt/shared/{g}" + for g in groups + ]) + spawner.init_containers = list(spawner.init_containers) + [{ + "name": "initialize-shared-mounts", + "image": "busybox:1.31", + "command": ["sh", "-c", mkdir_cmds], + "securityContext": {"runAsUser": 0}, + "volumeMounts": [ + { + "mountPath": f"/mnt/shared/{g}", + "name": "shared", + "subPath": f"shared/{g}", + } + for g in groups + ], + }] + + +def _generate_nss_files(username, uid=1000, gid=100): + """Generate /tmp/passwd and /tmp/group content for libnss_wrapper. + + Maps uid 1000 to the real username so whoami/id show the correct name. + The home dir is /home/jovyan to match the actual PVC mount point. + Additional supplemental GIDs suppress 'missing GID' warnings at startup. + """ + passwd = f"{username}:x:{uid}:{gid}:{username}:/home/jovyan:/bin/bash" + additional_gids = [4, 20, 24, 25, 27, 29, 30, 44, 46] + group_lines = [f"users:x:{gid}:"] + [f"nogroup{g}:x:{g}:" for g in additional_gids] + return passwd, "\n".join(group_lines) + + +async def _setup_nss_wrapper(spawner, username, has_shared_groups): + """Configure libnss_wrapper so whoami/id report the real username. + + Sets LD_PRELOAD and NB_UMASK, then adds a postStart lifecycle hook that + writes /tmp/passwd and /tmp/group and creates (or removes) the + /home/jovyan/shared symlink depending on whether the user has shared dirs. + """ + etc_passwd, etc_group = _generate_nss_files(username) + spawner.environment = { + **spawner.environment, + "LD_PRELOAD": "libnss_wrapper.so", + "NSS_WRAPPER_PASSWD": "/tmp/passwd", + "NSS_WRAPPER_GROUP": "/tmp/group", + "NB_UMASK": "0002", + } + + nss_cmds = [ + f"echo '{etc_passwd}' > /tmp/passwd", + f"echo '{etc_group}' > /tmp/group", + ] + if has_shared_groups: + nss_cmds.append(f"ln -sfn {shared_storage_mount_prefix} /home/jovyan/shared") + else: + nss_cmds.append("rm -f /home/jovyan/shared") + + spawner.lifecycle_hooks = { + "postStart": {"exec": {"command": ["/bin/sh", "-c", " && ".join(nss_cmds)]}} + } + + +# --------------------------------------------------------------------------- +# Pre-spawn hook orchestrator +# --------------------------------------------------------------------------- +# Chains the three independent concerns: Nebi auto-auth, shared storage mounts, +# and NSS wrapper setup. Each is implemented as its own focused function above. +# The orchestrator always runs so NSS wrapper is active even without Nebi/shared. + +_nebi_auth_configured = bool(nebi_remote_url and get_config("custom.nebi-internal-url", "")) + + +async def _pre_spawn_hook(spawner): + """Orchestrate all pre-spawn setup: Nebi auth, shared storage, NSS wrapper.""" + auth_state = await spawner.user.get_auth_state() + username = spawner.user.name + + # 1. Nebi auto-auth (no-op when not configured) + if _nebi_auth_configured: + await _nebi_pre_spawn_hook(spawner) + + # 2. Shared group directory mounts + groups = [] + if shared_storage_enabled: + groups = _get_user_groups(spawner, auth_state) + if groups: + await _setup_shared_storage(spawner, groups) + + # 3. NSS wrapper (always — makes whoami/id show the real username) + await _setup_nss_wrapper(spawner, username, bool(groups)) + + +c.KubeSpawner.pre_spawn_hook = _pre_spawn_hook diff --git a/values.yaml b/values.yaml index f6ee9f3..22afda3 100644 --- a/values.yaml +++ b/values.yaml @@ -59,6 +59,12 @@ sharedStorage: size: 10Gi accessModes: - ReadWriteMany + # Allowlist of group names to mount as shared directories. + # If empty, all groups from the user's Keycloak token are mounted. + # Groups are mounted at / inside user pods. + groups: [] + # Mount path prefix for shared group directories inside user pods. + mountPathPrefix: /shared # ============================================================================= # Nebi Integration @@ -104,6 +110,10 @@ jupyterhub: nebi-client-id: "" # Nebi's Keycloak OIDC client ID jupyterhub-client-id: "" # JupyterHub's Keycloak OIDC client ID nebi-environment-selector: false # Set true to show nebi workspaces in jhub-apps env dropdown + # Shared storage - set these to match top-level sharedStorage values. + shared-storage-enabled: false + shared-storage-groups: [] # allowlist; empty = all groups from token + shared-storage-mount-prefix: "/shared" # jhub-apps (JAppsConfig) overrides. # Any key here is set as an attribute on c.JAppsConfig before install_jhub_apps(). # See https://jhub-apps.nebari.dev/docs/configuration for available options. From 5fd6fc81d9c8d2b299ecb055c58f9ff052b85c7e Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Fri, 27 Mar 2026 13:38:47 +0000 Subject: [PATCH 02/60] fix: correct NSS GID to 1000 (jovyan) and always create ~/shared dir - gid default was 100 but z2jh sets pod securityContext GID to 1000; add jovyan:x:1000: group entry so 'groups' command resolves the name - when shared PVC is disabled, mkdir -p ~/shared instead of removing it so users always see the directory regardless of storage configuration --- config/jupyterhub/01-spawner.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/config/jupyterhub/01-spawner.py b/config/jupyterhub/01-spawner.py index e8f4e53..8fbbfff 100644 --- a/config/jupyterhub/01-spawner.py +++ b/config/jupyterhub/01-spawner.py @@ -587,16 +587,20 @@ async def _setup_shared_storage(spawner, groups): }] -def _generate_nss_files(username, uid=1000, gid=100): +def _generate_nss_files(username, uid=1000, gid=1000): """Generate /tmp/passwd and /tmp/group content for libnss_wrapper. Maps uid 1000 to the real username so whoami/id show the correct name. The home dir is /home/jovyan to match the actual PVC mount point. + gid=1000 matches the z2jh default securityContext (jovyan group). Additional supplemental GIDs suppress 'missing GID' warnings at startup. """ passwd = f"{username}:x:{uid}:{gid}:{username}:/home/jovyan:/bin/bash" additional_gids = [4, 20, 24, 25, 27, 29, 30, 44, 46] - group_lines = [f"users:x:{gid}:"] + [f"nogroup{g}:x:{g}:" for g in additional_gids] + group_lines = [ + f"jovyan:x:{gid}:", # primary group — matches z2jh pod securityContext GID + "users:x:100:", # supplemental group for shared directory access + ] + [f"nogroup{g}:x:{g}:" for g in additional_gids] return passwd, "\n".join(group_lines) @@ -621,9 +625,11 @@ async def _setup_nss_wrapper(spawner, username, has_shared_groups): f"echo '{etc_group}' > /tmp/group", ] if has_shared_groups: + # Symlink ~/shared → the PVC mount prefix so all group dirs are reachable nss_cmds.append(f"ln -sfn {shared_storage_mount_prefix} /home/jovyan/shared") else: - nss_cmds.append("rm -f /home/jovyan/shared") + # No shared PVC — create an empty placeholder dir so ~/shared exists + nss_cmds.append("mkdir -p /home/jovyan/shared") spawner.lifecycle_hooks = { "postStart": {"exec": {"command": ["/bin/sh", "-c", " && ".join(nss_cmds)]}} From 168694554e8ec58db7926252ae98d1d34e542ace Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Fri, 27 Mar 2026 13:44:46 +0000 Subject: [PATCH 03/60] fix: store groups in auth_state and always create ~/shared/ dirs - EnvoyOIDCAuthenticator now stores parsed groups in auth_state so the spawner can read them at spawn time (JupyterHub groups table is empty when manage_groups is not enabled) - refresh_user also re-parses groups from the refreshed IdToken to keep auth_state current - _pre_spawn_hook always resolves user groups, not only when shared PVC is enabled - _setup_nss_wrapper creates local ~/shared/ dirs per group when no shared PVC is configured, so users always see their group dirs --- config/jupyterhub/00-gateway-auth.py | 11 +++++++++ config/jupyterhub/01-spawner.py | 37 +++++++++++++++++----------- 2 files changed, 34 insertions(+), 14 deletions(-) diff --git a/config/jupyterhub/00-gateway-auth.py b/config/jupyterhub/00-gateway-auth.py index fa9e581..6024f50 100644 --- a/config/jupyterhub/00-gateway-auth.py +++ b/config/jupyterhub/00-gateway-auth.py @@ -114,6 +114,7 @@ async def authenticate(self, handler, data=None): "id_token": id_token, "access_token": access_token, "refresh_token": refresh_token, + "groups": groups, # stored so spawner can read at spawn time }.items() if v is not None }, } @@ -139,11 +140,21 @@ async def refresh_user(self, user, handler=None): self.log.warning("refresh_user: no IdToken cookie for %s, invalidating session", user.name) return False + # Re-parse groups from the refreshed IdToken so auth_state stays current + try: + payload_b64 = id_token.split(".")[1] + payload_b64 += "=" * (4 - len(payload_b64) % 4) + claims = json.loads(base64.urlsafe_b64decode(payload_b64)) + groups = [g.strip("/") for g in claims.get("groups", [])] + except Exception: + groups = [] + auth_state = { k: v for k, v in { "id_token": id_token, "access_token": access_token, "refresh_token": refresh_token, + "groups": groups, }.items() if v is not None } diff --git a/config/jupyterhub/01-spawner.py b/config/jupyterhub/01-spawner.py index 8fbbfff..0f3998e 100644 --- a/config/jupyterhub/01-spawner.py +++ b/config/jupyterhub/01-spawner.py @@ -604,12 +604,14 @@ def _generate_nss_files(username, uid=1000, gid=1000): return passwd, "\n".join(group_lines) -async def _setup_nss_wrapper(spawner, username, has_shared_groups): +async def _setup_nss_wrapper(spawner, username, groups): """Configure libnss_wrapper so whoami/id report the real username. - Sets LD_PRELOAD and NB_UMASK, then adds a postStart lifecycle hook that - writes /tmp/passwd and /tmp/group and creates (or removes) the - /home/jovyan/shared symlink depending on whether the user has shared dirs. + Sets LD_PRELOAD and NB_UMASK, then adds a postStart lifecycle hook that: + - writes /tmp/passwd and /tmp/group + - when shared PVC is enabled: symlinks ~/shared → PVC mount prefix + - when shared PVC is disabled: creates local ~/shared/ dirs so + users can see their group directories (not cross-user shared, but visible) """ etc_passwd, etc_group = _generate_nss_files(username) spawner.environment = { @@ -624,11 +626,16 @@ async def _setup_nss_wrapper(spawner, username, has_shared_groups): f"echo '{etc_passwd}' > /tmp/passwd", f"echo '{etc_group}' > /tmp/group", ] - if has_shared_groups: + + if groups and shared_storage_enabled: # Symlink ~/shared → the PVC mount prefix so all group dirs are reachable nss_cmds.append(f"ln -sfn {shared_storage_mount_prefix} /home/jovyan/shared") + elif groups: + # No shared PVC — create local per-group dirs under ~/shared + nss_cmds.append("mkdir -p /home/jovyan/shared") + for group in groups: + nss_cmds.append(f"mkdir -p /home/jovyan/shared/{group}") else: - # No shared PVC — create an empty placeholder dir so ~/shared exists nss_cmds.append("mkdir -p /home/jovyan/shared") spawner.lifecycle_hooks = { @@ -655,15 +662,17 @@ async def _pre_spawn_hook(spawner): if _nebi_auth_configured: await _nebi_pre_spawn_hook(spawner) - # 2. Shared group directory mounts - groups = [] - if shared_storage_enabled: - groups = _get_user_groups(spawner, auth_state) - if groups: - await _setup_shared_storage(spawner, groups) + # 2. Always resolve groups — needed for both shared storage mounts and + # local ~/shared/ dir creation in _setup_nss_wrapper + groups = _get_user_groups(spawner, auth_state) + + # 3. Shared group directory PVC mounts (only when RWX PVC is configured) + if shared_storage_enabled and groups: + await _setup_shared_storage(spawner, groups) - # 3. NSS wrapper (always — makes whoami/id show the real username) - await _setup_nss_wrapper(spawner, username, bool(groups)) + # 4. NSS wrapper (always — makes whoami/id show the real username, + # creates ~/shared/ dirs whether PVC-backed or local) + await _setup_nss_wrapper(spawner, username, groups) c.KubeSpawner.pre_spawn_hook = _pre_spawn_hook From d1f0b57ae72d9fcb980df9993ae85a64839e7a74 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Fri, 27 Mar 2026 13:51:40 +0000 Subject: [PATCH 04/60] feat: add in-cluster NFS server for shared storage on RWO-only clusters Deploys quay.io/nebari/volume-nfs backed by a single RWO PVC and re-exports it as RWX NFS, enabling shared group directories on providers like Hetzner that only provide ReadWriteOnce storage (hcloud-volumes). - templates/nfs-server.yaml: NFS Deployment, Service, backend RWO PVC - templates/shared-pvc.yaml: StorageClass + PV (NFS path) + PVC when nfsServer.enabled; falls back to external RWX PVC otherwise - values.yaml: sharedStorage.nfsServer.{enabled,storageClass,image} fields --- templates/nfs-server.yaml | 89 +++++++++++++++++++++++++++++++++++++++ templates/shared-pvc.yaml | 52 +++++++++++++++++++++++ values.yaml | 21 +++++++-- 3 files changed, 159 insertions(+), 3 deletions(-) create mode 100644 templates/nfs-server.yaml diff --git a/templates/nfs-server.yaml b/templates/nfs-server.yaml new file mode 100644 index 0000000..12b9f62 --- /dev/null +++ b/templates/nfs-server.yaml @@ -0,0 +1,89 @@ +{{- if and .Values.sharedStorage.enabled .Values.sharedStorage.nfsServer.enabled }} +# Backend RWO PVC — the actual disk the NFS server exports over the network. +# Uses the cluster's default StorageClass (or sharedStorage.nfsServer.storageClass +# if set) so it works on any provider including Hetzner hcloud-volumes. +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "nebari-data-science-pack.fullname" . }}-nfs-storage + labels: + {{- include "nebari-data-science-pack.labels" . | nindent 4 }} +spec: + accessModes: + - ReadWriteOnce + {{- if .Values.sharedStorage.nfsServer.storageClass }} + storageClassName: {{ .Values.sharedStorage.nfsServer.storageClass }} + {{- end }} + resources: + requests: + storage: {{ .Values.sharedStorage.size }} +--- +# NFS server Service — exposes NFS (2049), mountd (20048), rpcbind (111). +# The stable DNS name (-nfs..svc.cluster.local) is used +# by the PersistentVolume so all nodes can reach the NFS export. +apiVersion: v1 +kind: Service +metadata: + name: {{ include "nebari-data-science-pack.fullname" . }}-nfs + labels: + {{- include "nebari-data-science-pack.labels" . | nindent 4 }} +spec: + selector: + app.kubernetes.io/component: nfs-server + {{- include "nebari-data-science-pack.selectorLabels" . | nindent 4 }} + ports: + - name: nfs + port: 2049 + protocol: TCP + - name: mountd + port: 20048 + protocol: TCP + - name: rpcbind + port: 111 + protocol: TCP +--- +# NFS server Deployment — runs quay.io/nebari/volume-nfs in privileged mode, +# mounts the backend RWO PVC at /exports, and re-exports it as NFS. +# Single replica: the PVC is RWO so only one pod can write to it. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "nebari-data-science-pack.fullname" . }}-nfs + labels: + app.kubernetes.io/component: nfs-server + {{- include "nebari-data-science-pack.labels" . | nindent 4 }} +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/component: nfs-server + {{- include "nebari-data-science-pack.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + app.kubernetes.io/component: nfs-server + {{- include "nebari-data-science-pack.selectorLabels" . | nindent 8 }} + spec: + containers: + - name: nfs-server + image: {{ .Values.sharedStorage.nfsServer.image.repository }}:{{ .Values.sharedStorage.nfsServer.image.tag }} + ports: + - name: nfs + containerPort: 2049 + protocol: TCP + - name: mountd + containerPort: 20048 + protocol: TCP + - name: rpcbind + containerPort: 111 + protocol: TCP + securityContext: + privileged: true + volumeMounts: + - name: nfs-export + mountPath: /exports + volumes: + - name: nfs-export + persistentVolumeClaim: + claimName: {{ include "nebari-data-science-pack.fullname" . }}-nfs-storage +{{- end }} diff --git a/templates/shared-pvc.yaml b/templates/shared-pvc.yaml index 7a84a4b..ac92b4f 100644 --- a/templates/shared-pvc.yaml +++ b/templates/shared-pvc.yaml @@ -1,4 +1,55 @@ {{- if .Values.sharedStorage.enabled }} +{{- if .Values.sharedStorage.nfsServer.enabled }} +# Fake StorageClass used only for PV↔PVC binding — no dynamic provisioner. +# The unique name ensures this PVC only binds to our NFS-backed PV. +apiVersion: storage.k8s.io/v1 +kind: StorageClass +metadata: + name: {{ include "nebari-data-science-pack.fullname" . }}-nfs-share + labels: + {{- include "nebari-data-science-pack.labels" . | nindent 4 }} +provisioner: kubernetes.io/no-provisioner +volumeBindingMode: Immediate +--- +# PersistentVolume pointing to the in-cluster NFS server. +# Uses the service DNS name so it works regardless of which node the pod lands on. +# The fake StorageClass name ensures this PV only binds to our PVC below. +apiVersion: v1 +kind: PersistentVolume +metadata: + name: {{ include "nebari-data-science-pack.fullname" . }}-nfs-share + labels: + {{- include "nebari-data-science-pack.labels" . | nindent 4 }} +spec: + capacity: + storage: {{ .Values.sharedStorage.size }} + storageClassName: {{ include "nebari-data-science-pack.fullname" . }}-nfs-share + accessModes: + - ReadWriteMany + persistentVolumeReclaimPolicy: Retain + nfs: + server: {{ include "nebari-data-science-pack.fullname" . }}-nfs.{{ .Release.Namespace }}.svc.cluster.local + path: / +--- +# Shared-storage PVC — bound to the NFS-backed PV above. +# User pods mount this PVC with per-group subPaths (/shared/). +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: shared-storage + labels: + {{- include "nebari-data-science-pack.labels" . | nindent 4 }} +spec: + accessModes: + - ReadWriteMany + storageClassName: {{ include "nebari-data-science-pack.fullname" . }}-nfs-share + resources: + requests: + storage: {{ .Values.sharedStorage.size }} +{{- else }} +# External RWX StorageClass path — requires the cluster to provide an RWX-capable +# StorageClass (e.g. EFS, NFS provisioner). Set sharedStorage.nfsServer.enabled=true +# to use the built-in NFS server instead. apiVersion: v1 kind: PersistentVolumeClaim metadata: @@ -15,3 +66,4 @@ spec: requests: storage: {{ .Values.sharedStorage.size }} {{- end }} +{{- end }} diff --git a/values.yaml b/values.yaml index 22afda3..aee84cf 100644 --- a/values.yaml +++ b/values.yaml @@ -52,19 +52,34 @@ nebariapp: intervalSeconds: 30 timeoutSeconds: 5 -# Shared storage configuration (disabled - requires RWX storage class) +# Shared storage — per-group directories mounted at /shared/ in user pods. +# Requires an RWX volume so all user pods can mount it simultaneously. +# Use nfsServer.enabled=true (below) for an in-cluster NFS solution that works +# on providers with only RWO storage (e.g. Hetzner hcloud-volumes). sharedStorage: enabled: false - storageClass: "" # Leave empty to use cluster default + # storageClass for the shared PVC when nfsServer.enabled=false. + # Leave empty to use the cluster default (must support ReadWriteMany). + storageClass: "" size: 10Gi accessModes: - ReadWriteMany # Allowlist of group names to mount as shared directories. # If empty, all groups from the user's Keycloak token are mounted. - # Groups are mounted at / inside user pods. groups: [] # Mount path prefix for shared group directories inside user pods. mountPathPrefix: /shared + # In-cluster NFS server — deploys quay.io/nebari/volume-nfs backed by a + # single RWO PVC and re-exports it as RWX NFS to all user pods. + # Enables shared storage on providers that only have RWO StorageClasses. + nfsServer: + enabled: false + # StorageClass for the NFS server's backend RWO PVC. + # Leave empty to use the cluster default. + storageClass: "" + image: + repository: quay.io/nebari/volume-nfs + tag: "0.8-repack" # ============================================================================= # Nebi Integration From 57abb0637a726a49865f0cde9b9d383117d621f8 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Fri, 27 Mar 2026 13:56:57 +0000 Subject: [PATCH 05/60] fix: add DaemonSet to install nfs-common on k3s worker nodes k3s worker nodes on minimal OS images (Hetzner) ship without nfs-common, causing NFS PV mounts to fail with 'bad option'. The DaemonSet uses nsenter to install nfs-common on the host via apt-get, skipping if already present. Gated on sharedStorage.nfsServer.installClient (default false). --- templates/nfs-client-installer.yaml | 55 +++++++++++++++++++++++++++++ values.yaml | 3 ++ 2 files changed, 58 insertions(+) create mode 100644 templates/nfs-client-installer.yaml diff --git a/templates/nfs-client-installer.yaml b/templates/nfs-client-installer.yaml new file mode 100644 index 0000000..b8099d2 --- /dev/null +++ b/templates/nfs-client-installer.yaml @@ -0,0 +1,55 @@ +{{- if and .Values.sharedStorage.enabled .Values.sharedStorage.nfsServer.enabled .Values.sharedStorage.nfsServer.installClient }} +# DaemonSet that installs nfs-common on every node so kubelet can mount NFS PVs. +# k3s worker nodes typically ship without NFS client tools; this is the standard +# pattern for installing host-level dependencies without reprovisioning nodes. +# Runs in the host PID/network namespace (nsenter) to reach the node's package manager. +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: {{ include "nebari-data-science-pack.fullname" . }}-nfs-client-installer + labels: + {{- include "nebari-data-science-pack.labels" . | nindent 4 }} +spec: + selector: + matchLabels: + app.kubernetes.io/component: nfs-client-installer + {{- include "nebari-data-science-pack.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + app.kubernetes.io/component: nfs-client-installer + {{- include "nebari-data-science-pack.selectorLabels" . | nindent 8 }} + spec: + hostPID: true + hostNetwork: true + tolerations: + - operator: Exists + initContainers: + - name: install-nfs-common + image: ubuntu:22.04 + command: + - nsenter + - -m + - -u + - -i + - -n + - -p + - -t + - "1" + - -- + - bash + - -c + - | + if ! command -v mount.nfs >/dev/null 2>&1; then + apt-get update -qq && apt-get install -y -qq nfs-common + fi + securityContext: + privileged: true + containers: + - name: pause + image: gcr.io/google-containers/pause:3.9 + resources: + requests: + cpu: 1m + memory: 1Mi +{{- end }} diff --git a/values.yaml b/values.yaml index aee84cf..9d127e1 100644 --- a/values.yaml +++ b/values.yaml @@ -80,6 +80,9 @@ sharedStorage: image: repository: quay.io/nebari/volume-nfs tag: "0.8-repack" + # Deploy a DaemonSet that installs nfs-common on every node. + # Required on k3s/minimal OS nodes that ship without NFS client tools. + installClient: false # ============================================================================= # Nebi Integration From a13d11f99dd5e44519df148d2e4277ebd63c81d7 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Fri, 27 Mar 2026 13:59:00 +0000 Subject: [PATCH 06/60] fix: use alpine:3 sleep for DaemonSet pause container --- templates/nfs-client-installer.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/templates/nfs-client-installer.yaml b/templates/nfs-client-installer.yaml index b8099d2..7be94e8 100644 --- a/templates/nfs-client-installer.yaml +++ b/templates/nfs-client-installer.yaml @@ -47,7 +47,8 @@ spec: privileged: true containers: - name: pause - image: gcr.io/google-containers/pause:3.9 + image: alpine:3 + command: ["sh", "-c", "sleep infinity"] resources: requests: cpu: 1m From 75caabecfd9e94ed171407951c0db3f58b451b89 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Fri, 27 Mar 2026 14:06:25 +0000 Subject: [PATCH 07/60] fix: NFS PV path /exports not / (overlayfs cannot be exported) --- templates/shared-pvc.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/shared-pvc.yaml b/templates/shared-pvc.yaml index ac92b4f..a741b5b 100644 --- a/templates/shared-pvc.yaml +++ b/templates/shared-pvc.yaml @@ -29,7 +29,7 @@ spec: persistentVolumeReclaimPolicy: Retain nfs: server: {{ include "nebari-data-science-pack.fullname" . }}-nfs.{{ .Release.Namespace }}.svc.cluster.local - path: / + path: /exports --- # Shared-storage PVC — bound to the NFS-backed PV above. # User pods mount this PVC with per-group subPaths (/shared/). From 5b868343565a4772979e423ba43a580624a746fe Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Fri, 27 Mar 2026 14:15:31 +0000 Subject: [PATCH 08/60] fix: remove spawner.user.groups (DetachedInstanceError in async); add try-except _get_user_groups accessed spawner.user.groups (SQLAlchemy lazy-loaded relationship) from an async pre_spawn_hook, causing DetachedInstanceError which silently aborted _setup_shared_storage and _setup_nss_wrapper. Groups are now read only from auth_state (stored by EnvoyOIDCAuthenticator). Each step is individually wrapped in try-except so failures are logged and don't prevent subsequent steps from running. --- config/jupyterhub/01-spawner.py | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/config/jupyterhub/01-spawner.py b/config/jupyterhub/01-spawner.py index 0f3998e..6bec1bb 100644 --- a/config/jupyterhub/01-spawner.py +++ b/config/jupyterhub/01-spawner.py @@ -537,17 +537,17 @@ async def _nebi_pre_spawn_hook(spawner): # Shared storage hook helpers # --------------------------------------------------------------------------- -def _get_user_groups(spawner, auth_state): +def _get_user_groups(auth_state): """Extract and filter user groups from auth_state. - Returns groups from the Keycloak IdToken (set by EnvoyOIDCAuthenticator), - falling back to JupyterHub-managed groups. Applies the allowlist if configured. + Reads groups stored in auth_state by EnvoyOIDCAuthenticator (from Keycloak + IdToken groups claim). Applies the allowlist if configured. + Note: we do NOT fall back to spawner.user.groups because accessing the + SQLAlchemy relationship from an async pre_spawn_hook causes DetachedInstanceError. """ groups = [] if auth_state: groups = auth_state.get("groups", []) - if not groups: - groups = [g.name for g in spawner.user.groups] if shared_storage_groups_allowlist: groups = [g for g in groups if g in shared_storage_groups_allowlist] # Strip any leading slashes (Keycloak returns e.g. "/admin") @@ -658,21 +658,26 @@ async def _pre_spawn_hook(spawner): auth_state = await spawner.user.get_auth_state() username = spawner.user.name - # 1. Nebi auto-auth (no-op when not configured) + # 1. Nebi auto-auth (non-fatal) if _nebi_auth_configured: await _nebi_pre_spawn_hook(spawner) - # 2. Always resolve groups — needed for both shared storage mounts and - # local ~/shared/ dir creation in _setup_nss_wrapper - groups = _get_user_groups(spawner, auth_state) + # 2. Resolve groups from auth_state (stored by EnvoyOIDCAuthenticator) + groups = _get_user_groups(auth_state) # 3. Shared group directory PVC mounts (only when RWX PVC is configured) if shared_storage_enabled and groups: - await _setup_shared_storage(spawner, groups) + try: + await _setup_shared_storage(spawner, groups) + except Exception: + log.exception("Failed to set up shared storage for %s (pod will still spawn)", username) - # 4. NSS wrapper (always — makes whoami/id show the real username, - # creates ~/shared/ dirs whether PVC-backed or local) - await _setup_nss_wrapper(spawner, username, groups) + # 4. NSS wrapper — always runs; wrapped independently so shared storage + # failures don't prevent whoami/id from showing the real username + try: + await _setup_nss_wrapper(spawner, username, groups) + except Exception: + log.exception("Failed to set up NSS wrapper for %s", username) c.KubeSpawner.pre_spawn_hook = _pre_spawn_hook From fc7a1690fbd8b4d64d3b2796149e886ff57d6cd7 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Fri, 27 Mar 2026 16:04:32 +0000 Subject: [PATCH 09/60] fix: address code review findings (I1-I7, C1-C3, M1, N3) I1: set c.KubeSpawner.fs_gid=100 explicitly so shared dir file ownership is deterministic (GID 100 = users group) rather than relying on z2jh default I2: add Helm validation in _helpers.tpl that fails at template time if sharedStorage.enabled and jupyterhub.custom.shared-storage-enabled diverge I3: use Path(g).name like classic Nebari so /projects/myproj -> myproj, not projects/myproj; deduplicate groups to prevent duplicate mountPaths I4: add nodeSelector/nodeAffinity support to NFS server Deployment so deployers can pin it to worker nodes and avoid slow RWO PVC reattachment I7: add argocd.argoproj.io/sync-options: Prune=false to StorageClass and PersistentVolume to prevent accidental deletion during ArgoCD force sync C1: add chown 0:100 before chmod 2775 in initialize-shared-mounts init container so shared dirs are explicitly owned by GID 100 (users) C2: use printf instead of echo '...' for NSS file writes to safely handle special characters in usernames without shell quoting issues C3: deduplicate groups in _get_user_groups (via Path.name already handles most cases; added explicit dedup set for belt-and-suspenders) M1: log exception with exc_info=True in refresh_user JWT parse failure N3: merge into existing lifecycle_hooks instead of replacing; warn if a postStart hook already exists before overwriting Logging: added comprehensive info/debug/warning logging throughout all pre-spawn hook functions for both happy and failure paths --- config/jupyterhub/00-gateway-auth.py | 29 ++++- config/jupyterhub/01-spawner.py | 163 ++++++++++++++++++++++----- templates/_helpers.tpl | 17 +++ templates/hub-config.yaml | 1 + templates/nfs-server.yaml | 9 ++ templates/shared-pvc.yaml | 9 ++ values.yaml | 11 +- 7 files changed, 206 insertions(+), 33 deletions(-) diff --git a/config/jupyterhub/00-gateway-auth.py b/config/jupyterhub/00-gateway-auth.py index 6024f50..2898d05 100644 --- a/config/jupyterhub/00-gateway-auth.py +++ b/config/jupyterhub/00-gateway-auth.py @@ -93,17 +93,27 @@ async def authenticate(self, handler, data=None): username = claims.get("preferred_username") or claims.get("sub") if not username: - self.log.warning("No username claim in IdToken: %s", list(claims.keys())) + self.log.warning("authenticate: no username claim in IdToken: %s", list(claims.keys())) return None # Extract groups from the token (set by the "groups" scope / group mapper) groups = claims.get("groups", []) + if not groups: + self.log.warning( + "authenticate: no groups claim in IdToken for %s " + "(check Keycloak client scope / group mapper configuration)", + username, + ) # Keycloak returns groups as paths (e.g. "/admin"), strip leading slash groups = [g.strip("/") for g in groups] # Determine admin from group membership admin_groups = set(get_config("custom.admin-groups", ["admin"])) is_admin = bool(admin_groups & set(groups)) + self.log.info( + "authenticate: user=%s groups=%s is_admin=%s", + username, groups, is_admin, + ) return { "name": username, @@ -131,13 +141,17 @@ async def refresh_user(self, user, handler=None): """ if handler is None: # No request context (e.g. internal API call) — can't read cookies + self.log.debug("refresh_user: no handler for %s (internal call), returning True", user.name) return True id_token, access_token, refresh_token = self._extract_envoy_cookies(handler) if not id_token: # Cookies missing — session may have expired, force re-login - self.log.warning("refresh_user: no IdToken cookie for %s, invalidating session", user.name) + self.log.warning( + "refresh_user: no IdToken cookie for %s — session likely expired, forcing re-login", + user.name, + ) return False # Re-parse groups from the refreshed IdToken so auth_state stays current @@ -146,7 +160,14 @@ async def refresh_user(self, user, handler=None): payload_b64 += "=" * (4 - len(payload_b64) % 4) claims = json.loads(base64.urlsafe_b64decode(payload_b64)) groups = [g.strip("/") for g in claims.get("groups", [])] + self.log.debug("refresh_user: refreshed groups for %s: %s", user.name, groups) except Exception: + self.log.warning( + "refresh_user: failed to parse groups from IdToken for %s " + "— auth_state will have empty groups until next full login", + user.name, + exc_info=True, + ) groups = [] auth_state = { @@ -158,6 +179,10 @@ async def refresh_user(self, user, handler=None): }.items() if v is not None } + self.log.debug( + "refresh_user: auth_state refreshed for %s (access_token=%s, groups=%s)", + user.name, bool(access_token), groups, + ) return {"name": user.name, "auth_state": auth_state} diff --git a/config/jupyterhub/01-spawner.py b/config/jupyterhub/01-spawner.py index 6bec1bb..77cdccc 100644 --- a/config/jupyterhub/01-spawner.py +++ b/config/jupyterhub/01-spawner.py @@ -7,6 +7,7 @@ import os import urllib.error import urllib.request +from pathlib import Path from urllib.parse import urlencode from urllib.request import Request, urlopen @@ -90,6 +91,15 @@ "persistentVolumeClaim": {"claimName": "shared-storage"}, }) +# GID 100 (users group) as the fsGroup for all singleuser pods. +# Kubernetes applies this as securityContext.fsGroup, which: +# 1. Adds GID 100 as a supplemental group for the process +# 2. chgrp -R 100 on all mounted volumes at pod start +# This ensures shared dirs (created by init container as root:root) become +# root:100, making them writable by users who have GID 100. Without this +# being explicit, the behavior relies silently on the z2jh subchart default. +c.KubeSpawner.fs_gid = 100 + # --------------------------------------------------------------------------- # Nebi binary (init container) @@ -542,33 +552,67 @@ def _get_user_groups(auth_state): Reads groups stored in auth_state by EnvoyOIDCAuthenticator (from Keycloak IdToken groups claim). Applies the allowlist if configured. - Note: we do NOT fall back to spawner.user.groups because accessing the - SQLAlchemy relationship from an async pre_spawn_hook causes DetachedInstanceError. + Uses Path(g).name (last component) like classic Nebari so /projects/myproj → myproj. + Deduplicates to prevent duplicate mountPath entries in the pod spec. + Note: does NOT fall back to spawner.user.groups — accessing that SQLAlchemy + relationship from an async hook causes DetachedInstanceError. """ - groups = [] + raw_groups = [] if auth_state: - groups = auth_state.get("groups", []) + raw_groups = auth_state.get("groups", []) + log.debug("shared-storage: raw groups from auth_state: %s", raw_groups) + else: + log.debug("shared-storage: no auth_state (DummyAuthenticator?), groups will be empty") + if shared_storage_groups_allowlist: - groups = [g for g in groups if g in shared_storage_groups_allowlist] - # Strip any leading slashes (Keycloak returns e.g. "/admin") - return [g.strip("/") for g in groups if g] + before = raw_groups + raw_groups = [g for g in raw_groups if g in shared_storage_groups_allowlist] + log.debug( + "shared-storage: allowlist %s filtered %s → %s", + shared_storage_groups_allowlist, before, raw_groups, + ) + + seen = set() + result = [] + for g in raw_groups: + name = Path(g).name + if not name: + log.debug("shared-storage: skipping empty group name from %r", g) + continue + if name in seen: + log.debug("shared-storage: deduplicating group %r (already have it)", name) + continue + seen.add(name) + result.append(name) + + log.info("shared-storage: resolved groups for user: %s", result) + return result async def _setup_shared_storage(spawner, groups): """Add per-group volume mounts and a single init container for shared directories. - Creates /shared/ on the PVC (chmod 2775 so new files inherit the - group and are group-writable with NB_UMASK=0002). + Creates /shared/ on the PVC with: + - chown 0:100 so group owner is GID 100 (users), matching pod fs_gid + - chmod 2775 (rwxrwsr-x) so group has write and setgid propagates GID to new files + Combined with NB_UMASK=0002, new files are group-writable (664/775). """ + log.info( + "shared-storage: setting up PVC mounts for user %s, groups: %s", + spawner.user.name, groups, + ) for group in groups: spawner.volume_mounts = list(spawner.volume_mounts) + [{ "mountPath": f"{shared_storage_mount_prefix}/{group}", "name": "shared", "subPath": f"shared/{group}", }] + log.debug("shared-storage: added volume mount for group %r", group) + # chown 0:100 sets GID 100 (users group) as owner, matching the pod's fs_gid=100. + # chmod 2775: group has rwx + setgid bit so new files inherit GID 100. mkdir_cmds = " && ".join([ - f"mkdir -p /mnt/shared/{g} && chmod 2775 /mnt/shared/{g}" + f"mkdir -p /mnt/shared/{g} && chown 0:100 /mnt/shared/{g} && chmod 2775 /mnt/shared/{g}" for g in groups ]) spawner.init_containers = list(spawner.init_containers) + [{ @@ -585,21 +629,26 @@ async def _setup_shared_storage(spawner, groups): for g in groups ], }] + log.info( + "shared-storage: added initialize-shared-mounts init container for groups: %s", + groups, + ) def _generate_nss_files(username, uid=1000, gid=1000): """Generate /tmp/passwd and /tmp/group content for libnss_wrapper. Maps uid 1000 to the real username so whoami/id show the correct name. - The home dir is /home/jovyan to match the actual PVC mount point. - gid=1000 matches the z2jh default securityContext (jovyan group). + Home dir is /home/jovyan to match the actual PVC mount point. + GID 1000 (jovyan) matches the pod's primary GID from the container image. + GID 100 (users) is included as a supplemental group for shared dir access. Additional supplemental GIDs suppress 'missing GID' warnings at startup. """ passwd = f"{username}:x:{uid}:{gid}:{username}:/home/jovyan:/bin/bash" additional_gids = [4, 20, 24, 25, 27, 29, 30, 44, 46] group_lines = [ - f"jovyan:x:{gid}:", # primary group — matches z2jh pod securityContext GID - "users:x:100:", # supplemental group for shared directory access + f"jovyan:x:{gid}:", # primary group — matches pod securityContext GID 1000 + "users:x:100:", # supplemental — needed for shared dir write access ] + [f"nogroup{g}:x:{g}:" for g in additional_gids] return passwd, "\n".join(group_lines) @@ -607,13 +656,22 @@ def _generate_nss_files(username, uid=1000, gid=1000): async def _setup_nss_wrapper(spawner, username, groups): """Configure libnss_wrapper so whoami/id report the real username. - Sets LD_PRELOAD and NB_UMASK, then adds a postStart lifecycle hook that: - - writes /tmp/passwd and /tmp/group + Sets LD_PRELOAD, NSS_WRAPPER_* paths, and NB_UMASK=0002. + Adds a postStart lifecycle hook that: + - writes /tmp/passwd and /tmp/group using printf (safe for special chars in username) - when shared PVC is enabled: symlinks ~/shared → PVC mount prefix - when shared PVC is disabled: creates local ~/shared/ dirs so users can see their group directories (not cross-user shared, but visible) + Merges into existing lifecycle_hooks to avoid overwriting hooks set elsewhere. """ + log.info( + "nss-wrapper: configuring for user %s (groups=%s, shared_storage=%s)", + username, groups, shared_storage_enabled, + ) + etc_passwd, etc_group = _generate_nss_files(username) + log.debug("nss-wrapper: passwd entry: %s", etc_passwd) + spawner.environment = { **spawner.environment, "LD_PRELOAD": "libnss_wrapper.so", @@ -621,26 +679,40 @@ async def _setup_nss_wrapper(spawner, username, groups): "NSS_WRAPPER_GROUP": "/tmp/group", "NB_UMASK": "0002", } + log.debug("nss-wrapper: LD_PRELOAD and NSS_WRAPPER_* set in spawner environment") + # Use printf instead of echo '...' to safely handle special characters in + # usernames (e.g. '@', colons) without shell quoting issues. nss_cmds = [ - f"echo '{etc_passwd}' > /tmp/passwd", - f"echo '{etc_group}' > /tmp/group", + f"printf '%s\\n' {etc_passwd!r} > /tmp/passwd", + f"printf '%s\\n' {etc_group!r} > /tmp/group", ] if groups and shared_storage_enabled: - # Symlink ~/shared → the PVC mount prefix so all group dirs are reachable + log.debug("nss-wrapper: symlinking ~/shared → %s (PVC-backed)", shared_storage_mount_prefix) nss_cmds.append(f"ln -sfn {shared_storage_mount_prefix} /home/jovyan/shared") elif groups: - # No shared PVC — create local per-group dirs under ~/shared + log.debug("nss-wrapper: creating local ~/shared/ dirs (no PVC): %s", groups) nss_cmds.append("mkdir -p /home/jovyan/shared") for group in groups: nss_cmds.append(f"mkdir -p /home/jovyan/shared/{group}") else: + log.debug("nss-wrapper: no groups — creating empty ~/shared dir") nss_cmds.append("mkdir -p /home/jovyan/shared") + # Merge into existing lifecycle_hooks rather than replacing, so other + # hooks (e.g. from future jhub-apps versions) are not silently overwritten. + existing = dict(getattr(spawner, "lifecycle_hooks", None) or {}) + if "postStart" in existing: + log.warning( + "nss-wrapper: existing postStart lifecycle hook found for %s — overwriting", + username, + ) spawner.lifecycle_hooks = { - "postStart": {"exec": {"command": ["/bin/sh", "-c", " && ".join(nss_cmds)]}} + **existing, + "postStart": {"exec": {"command": ["/bin/sh", "-c", " && ".join(nss_cmds)]}}, } + log.info("nss-wrapper: postStart lifecycle hook registered for %s", username) # --------------------------------------------------------------------------- @@ -651,33 +723,64 @@ async def _setup_nss_wrapper(spawner, username, groups): # The orchestrator always runs so NSS wrapper is active even without Nebi/shared. _nebi_auth_configured = bool(nebi_remote_url and get_config("custom.nebi-internal-url", "")) +log.info( + "pre-spawn: Nebi auth configured=%s, shared storage enabled=%s, mount prefix=%s", + _nebi_auth_configured, shared_storage_enabled, shared_storage_mount_prefix, +) async def _pre_spawn_hook(spawner): """Orchestrate all pre-spawn setup: Nebi auth, shared storage, NSS wrapper.""" - auth_state = await spawner.user.get_auth_state() username = spawner.user.name + log.info("pre-spawn: starting hook for user %s", username) + + auth_state = await spawner.user.get_auth_state() + if not auth_state: + log.warning("pre-spawn: no auth_state for %s (DummyAuthenticator or auth state disabled)", username) + else: + log.debug("pre-spawn: auth_state keys for %s: %s", username, list(auth_state.keys())) # 1. Nebi auto-auth (non-fatal) if _nebi_auth_configured: + log.debug("pre-spawn: running Nebi auto-auth for %s", username) await _nebi_pre_spawn_hook(spawner) + else: + log.debug("pre-spawn: Nebi auto-auth not configured, skipping") # 2. Resolve groups from auth_state (stored by EnvoyOIDCAuthenticator) groups = _get_user_groups(auth_state) + log.info("pre-spawn: user %s resolved groups: %s", username, groups) # 3. Shared group directory PVC mounts (only when RWX PVC is configured) - if shared_storage_enabled and groups: - try: - await _setup_shared_storage(spawner, groups) - except Exception: - log.exception("Failed to set up shared storage for %s (pod will still spawn)", username) + if shared_storage_enabled: + if groups: + log.info("pre-spawn: setting up shared storage mounts for %s", username) + try: + await _setup_shared_storage(spawner, groups) + log.info("pre-spawn: shared storage mounts configured for %s", username) + except Exception: + log.exception( + "pre-spawn: shared storage setup FAILED for %s (pod will still spawn)", + username, + ) + else: + log.info( + "pre-spawn: shared storage enabled but user %s has no groups — skipping PVC mounts", + username, + ) + else: + log.debug("pre-spawn: shared storage disabled, skipping PVC mounts for %s", username) - # 4. NSS wrapper — always runs; wrapped independently so shared storage - # failures don't prevent whoami/id from showing the real username + # 4. NSS wrapper — always runs; independently guarded so shared storage + # failures never prevent whoami/id from showing the real username + log.debug("pre-spawn: running NSS wrapper setup for %s", username) try: await _setup_nss_wrapper(spawner, username, groups) + log.info("pre-spawn: NSS wrapper configured for %s", username) except Exception: - log.exception("Failed to set up NSS wrapper for %s", username) + log.exception("pre-spawn: NSS wrapper setup FAILED for %s", username) + + log.info("pre-spawn: hook complete for user %s", username) c.KubeSpawner.pre_spawn_hook = _pre_spawn_hook diff --git a/templates/_helpers.tpl b/templates/_helpers.tpl index 4567b8a..e6e5e11 100644 --- a/templates/_helpers.tpl +++ b/templates/_helpers.tpl @@ -42,6 +42,23 @@ app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} app.kubernetes.io/managed-by: {{ .Release.Service }} {{- end }} +{{/* +Validate shared storage configuration consistency. +sharedStorage.enabled (controls PVC creation) and +jupyterhub.custom.shared-storage-enabled (controls spawner mounts) must match. +A mismatch causes either a missing PVC error on every spawn or a dormant PVC. +*/}} +{{- define "nebari-data-science-pack.validateSharedStorage" -}} +{{- $pvEnabled := .Values.sharedStorage.enabled -}} +{{- $spawnEnabled := index .Values.jupyterhub.custom "shared-storage-enabled" | default false -}} +{{- if and $pvEnabled (not $spawnEnabled) -}} +{{- fail "Configuration mismatch: sharedStorage.enabled=true but jupyterhub.custom.shared-storage-enabled=false. Set both to true or both to false." -}} +{{- end -}} +{{- if and $spawnEnabled (not $pvEnabled) -}} +{{- fail "Configuration mismatch: jupyterhub.custom.shared-storage-enabled=true but sharedStorage.enabled=false. Set both to true or both to false." -}} +{{- end -}} +{{- end -}} + {{/* Selector labels */}} diff --git a/templates/hub-config.yaml b/templates/hub-config.yaml index 97244ec..87bf299 100644 --- a/templates/hub-config.yaml +++ b/templates/hub-config.yaml @@ -1,3 +1,4 @@ +{{- include "nebari-data-science-pack.validateSharedStorage" . }} apiVersion: v1 kind: ConfigMap metadata: diff --git a/templates/nfs-server.yaml b/templates/nfs-server.yaml index 12b9f62..af707c7 100644 --- a/templates/nfs-server.yaml +++ b/templates/nfs-server.yaml @@ -64,6 +64,15 @@ spec: app.kubernetes.io/component: nfs-server {{- include "nebari-data-science-pack.selectorLabels" . | nindent 8 }} spec: + {{- if .Values.sharedStorage.nfsServer.nodeSelector }} + nodeSelector: + {{- toYaml .Values.sharedStorage.nfsServer.nodeSelector | nindent 8 }} + {{- end }} + {{- if .Values.sharedStorage.nfsServer.nodeAffinity }} + affinity: + nodeAffinity: + {{- toYaml .Values.sharedStorage.nfsServer.nodeAffinity | nindent 10 }} + {{- end }} containers: - name: nfs-server image: {{ .Values.sharedStorage.nfsServer.image.repository }}:{{ .Values.sharedStorage.nfsServer.image.tag }} diff --git a/templates/shared-pvc.yaml b/templates/shared-pvc.yaml index a741b5b..126aaa6 100644 --- a/templates/shared-pvc.yaml +++ b/templates/shared-pvc.yaml @@ -8,6 +8,10 @@ metadata: name: {{ include "nebari-data-science-pack.fullname" . }}-nfs-share labels: {{- include "nebari-data-science-pack.labels" . | nindent 4 }} + annotations: + # Never delete this StorageClass during ArgoCD sync — deleting it unbinds the + # shared-storage PVC and disrupts all user pods until manually recreated. + argocd.argoproj.io/sync-options: Prune=false provisioner: kubernetes.io/no-provisioner volumeBindingMode: Immediate --- @@ -20,6 +24,11 @@ metadata: name: {{ include "nebari-data-science-pack.fullname" . }}-nfs-share labels: {{- include "nebari-data-science-pack.labels" . | nindent 4 }} + annotations: + # Never delete this PV during ArgoCD sync — deletion unbinds the PVC and + # disrupts all user pods. Data on the backing NFS PVC is NOT lost (Retain + # policy) but the platform is down until the PV is manually recreated. + argocd.argoproj.io/sync-options: Prune=false spec: capacity: storage: {{ .Values.sharedStorage.size }} diff --git a/values.yaml b/values.yaml index 9d127e1..1bac377 100644 --- a/values.yaml +++ b/values.yaml @@ -83,6 +83,14 @@ sharedStorage: # Deploy a DaemonSet that installs nfs-common on every node. # Required on k3s/minimal OS nodes that ship without NFS client tools. installClient: false + # Pin the NFS server pod to specific nodes to avoid slow RWO PVC reattachment + # when the pod reschedules (detach + reattach can take 30-120s on some providers). + # Example for Hetzner worker nodes: + # nodeSelector: + # node.kubernetes.io/instance-type: cpx31 + nodeSelector: {} + # Advanced: full nodeAffinity spec (overrides nodeSelector if both set) + nodeAffinity: {} # ============================================================================= # Nebi Integration @@ -128,7 +136,8 @@ jupyterhub: nebi-client-id: "" # Nebi's Keycloak OIDC client ID jupyterhub-client-id: "" # JupyterHub's Keycloak OIDC client ID nebi-environment-selector: false # Set true to show nebi workspaces in jhub-apps env dropdown - # Shared storage - set these to match top-level sharedStorage values. + # Shared storage — MUST match top-level sharedStorage.* values exactly. + # helm template will fail if sharedStorage.enabled and shared-storage-enabled diverge. shared-storage-enabled: false shared-storage-groups: [] # allowlist; empty = all groups from token shared-storage-mount-prefix: "/shared" From 56c22cf32928e73947c2779eb8ace524a0b2b88b Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Fri, 27 Mar 2026 16:40:08 +0000 Subject: [PATCH 10/60] docs: add JupyterLab profiles design spec --- .../2026-03-27-jupyterlab-profiles-design.md | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 docs/superpowers/specs/2026-03-27-jupyterlab-profiles-design.md diff --git a/docs/superpowers/specs/2026-03-27-jupyterlab-profiles-design.md b/docs/superpowers/specs/2026-03-27-jupyterlab-profiles-design.md new file mode 100644 index 0000000..6d0fe5a --- /dev/null +++ b/docs/superpowers/specs/2026-03-27-jupyterlab-profiles-design.md @@ -0,0 +1,90 @@ +# JupyterLab Profiles — Design Spec + +**Date:** 2026-03-27 +**Status:** Approved + +## Goal + +Expose a profile selector in JupyterHub so users can choose their resource allocation (CPU / RAM) before spawning a JupyterLab pod. Matches the profile experience from classic Nebari. + +## Scope + +- CPU and memory sizing only (no GPU, no custom images in this iteration) +- Access model: `all` — every authenticated user sees every profile +- Two default profiles: Small and Medium +- Deployers can override the full list via their deployment values file + +## Out of Scope + +- Per-user or per-group access control (`yaml` / `keycloak` access modes) +- GPU profiles (`node_selector`, `extra_resource_limits`) +- Custom image per profile +- Profile options (sub-choices within a profile) + +## Design + +### values.yaml + +Add `profiles` list under `jupyterhub.custom.profiles`. Default ships two profiles: + +```yaml +jupyterhub: + custom: + profiles: + - display_name: "Small" + description: "1 CPU / 2 GB RAM" + default: true + kubespawner_override: + cpu_limit: 1 + cpu_guarantee: 0.5 + mem_limit: "2G" + mem_guarantee: "1G" + - display_name: "Medium" + description: "4 CPU / 8 GB RAM" + kubespawner_override: + cpu_limit: 4 + cpu_guarantee: 2 + mem_limit: "8G" + mem_guarantee: "4G" +``` + +Deployers override the entire list in their deployment-specific values file. Any valid KubeSpawner trait is accepted in `kubespawner_override` — this means GPU (`extra_resource_limits`), `node_selector`, and `image` all work in the future without code changes. + +### config/jupyterhub/01-spawner.py + +Add one section after the existing environment variable setup: + +```python +# --------------------------------------------------------------------------- +# Profiles (resource sizing) +# --------------------------------------------------------------------------- +profiles = get_config("custom.profiles", []) +if profiles: + c.KubeSpawner.profile_list = profiles + log.info("profiles: loaded %d profile(s)", len(profiles)) +else: + log.info("profiles: none configured, single-instance mode") +``` + +No other changes. KubeSpawner applies `kubespawner_override` natively. + +### Interaction with pre_spawn_hook + +KubeSpawner applies `kubespawner_override` (cpu/mem) first, then calls `pre_spawn_hook`. The existing hook (NSS wrapper, shared storage mounts, Nebi auth) runs on top of whichever profile the user picked. No conflict. + +### Behaviour when profiles list is empty + +If `custom.profiles` is absent or `[]`, `c.KubeSpawner.profile_list` is not set and JupyterHub spawns a single pod with no selector shown. Existing behaviour is fully preserved. + +## Files Changed + +| File | Change | +|------|--------| +| `values.yaml` | Add `jupyterhub.custom.profiles` with Small and Medium defaults | +| `config/jupyterhub/01-spawner.py` | Read profiles, set `c.KubeSpawner.profile_list` if non-empty | + +## Future Extensions (not in scope now) + +- Add `access: yaml` and `access: keycloak` support by replacing the static list with an async `render_profiles(spawner)` callable — no values.yaml changes needed +- GPU profiles: add `node_selector` and `extra_resource_limits` to a profile's `kubespawner_override` — already supported by KubeSpawner, zero code changes +- Custom image per profile: add `image` to `kubespawner_override` — same story From f46cdc41d3029150577e1eb953001a3a6904698a Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Fri, 27 Mar 2026 16:40:52 +0000 Subject: [PATCH 11/60] Revert "docs: add JupyterLab profiles design spec" This reverts commit 56c22cf32928e73947c2779eb8ace524a0b2b88b. --- .../2026-03-27-jupyterlab-profiles-design.md | 90 ------------------- 1 file changed, 90 deletions(-) delete mode 100644 docs/superpowers/specs/2026-03-27-jupyterlab-profiles-design.md diff --git a/docs/superpowers/specs/2026-03-27-jupyterlab-profiles-design.md b/docs/superpowers/specs/2026-03-27-jupyterlab-profiles-design.md deleted file mode 100644 index 6d0fe5a..0000000 --- a/docs/superpowers/specs/2026-03-27-jupyterlab-profiles-design.md +++ /dev/null @@ -1,90 +0,0 @@ -# JupyterLab Profiles — Design Spec - -**Date:** 2026-03-27 -**Status:** Approved - -## Goal - -Expose a profile selector in JupyterHub so users can choose their resource allocation (CPU / RAM) before spawning a JupyterLab pod. Matches the profile experience from classic Nebari. - -## Scope - -- CPU and memory sizing only (no GPU, no custom images in this iteration) -- Access model: `all` — every authenticated user sees every profile -- Two default profiles: Small and Medium -- Deployers can override the full list via their deployment values file - -## Out of Scope - -- Per-user or per-group access control (`yaml` / `keycloak` access modes) -- GPU profiles (`node_selector`, `extra_resource_limits`) -- Custom image per profile -- Profile options (sub-choices within a profile) - -## Design - -### values.yaml - -Add `profiles` list under `jupyterhub.custom.profiles`. Default ships two profiles: - -```yaml -jupyterhub: - custom: - profiles: - - display_name: "Small" - description: "1 CPU / 2 GB RAM" - default: true - kubespawner_override: - cpu_limit: 1 - cpu_guarantee: 0.5 - mem_limit: "2G" - mem_guarantee: "1G" - - display_name: "Medium" - description: "4 CPU / 8 GB RAM" - kubespawner_override: - cpu_limit: 4 - cpu_guarantee: 2 - mem_limit: "8G" - mem_guarantee: "4G" -``` - -Deployers override the entire list in their deployment-specific values file. Any valid KubeSpawner trait is accepted in `kubespawner_override` — this means GPU (`extra_resource_limits`), `node_selector`, and `image` all work in the future without code changes. - -### config/jupyterhub/01-spawner.py - -Add one section after the existing environment variable setup: - -```python -# --------------------------------------------------------------------------- -# Profiles (resource sizing) -# --------------------------------------------------------------------------- -profiles = get_config("custom.profiles", []) -if profiles: - c.KubeSpawner.profile_list = profiles - log.info("profiles: loaded %d profile(s)", len(profiles)) -else: - log.info("profiles: none configured, single-instance mode") -``` - -No other changes. KubeSpawner applies `kubespawner_override` natively. - -### Interaction with pre_spawn_hook - -KubeSpawner applies `kubespawner_override` (cpu/mem) first, then calls `pre_spawn_hook`. The existing hook (NSS wrapper, shared storage mounts, Nebi auth) runs on top of whichever profile the user picked. No conflict. - -### Behaviour when profiles list is empty - -If `custom.profiles` is absent or `[]`, `c.KubeSpawner.profile_list` is not set and JupyterHub spawns a single pod with no selector shown. Existing behaviour is fully preserved. - -## Files Changed - -| File | Change | -|------|--------| -| `values.yaml` | Add `jupyterhub.custom.profiles` with Small and Medium defaults | -| `config/jupyterhub/01-spawner.py` | Read profiles, set `c.KubeSpawner.profile_list` if non-empty | - -## Future Extensions (not in scope now) - -- Add `access: yaml` and `access: keycloak` support by replacing the static list with an async `render_profiles(spawner)` callable — no values.yaml changes needed -- GPU profiles: add `node_selector` and `extra_resource_limits` to a profile's `kubespawner_override` — already supported by KubeSpawner, zero code changes -- Custom image per profile: add `image` to `kubespawner_override` — same story From 7809fa34272e2f6b6af1af1fcdcf17fe33a48185 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Fri, 27 Mar 2026 16:42:03 +0000 Subject: [PATCH 12/60] feat: add JupyterLab profiles for CPU/RAM resource sizing (closes #31) Exposes a profile selector in JupyterHub matching the classic Nebari experience. Profiles are defined under jupyterhub.custom.profiles in values.yaml and passed directly to c.KubeSpawner.profile_list via get_config(). Default profiles: - Small: 1 CPU / 2 GB RAM (default) - Medium: 4 CPU / 8 GB RAM kubespawner_override accepts any KubeSpawner trait so GPU profiles, custom images, and node selectors work without code changes in the future. When profiles list is empty, no selector is shown (single-instance mode). --- config/jupyterhub/01-spawner.py | 15 +++++++++++++++ values.yaml | 22 ++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/config/jupyterhub/01-spawner.py b/config/jupyterhub/01-spawner.py index 77cdccc..1865267 100644 --- a/config/jupyterhub/01-spawner.py +++ b/config/jupyterhub/01-spawner.py @@ -145,6 +145,21 @@ c.KubeSpawner.environment = env +# --------------------------------------------------------------------------- +# Profiles (resource sizing) +# --------------------------------------------------------------------------- +# Each profile maps directly to a KubeSpawner profile_list entry. +# kubespawner_override accepts any valid KubeSpawner trait so deployers can +# add node_selector, image, extra_resource_limits (GPU), etc. without code changes. +# When the list is empty, no profile selector is shown (single-instance mode). +_profiles = get_config("custom.profiles", []) +if _profiles: + c.KubeSpawner.profile_list = _profiles + log.info("profiles: loaded %d profile(s): %s", len(_profiles), [p.get("display_name") for p in _profiles]) +else: + log.info("profiles: none configured — single-instance mode") + + # --------------------------------------------------------------------------- # Keycloak token exchange helpers (synchronous, shared) # --------------------------------------------------------------------------- diff --git a/values.yaml b/values.yaml index 1bac377..736be5d 100644 --- a/values.yaml +++ b/values.yaml @@ -136,6 +136,28 @@ jupyterhub: nebi-client-id: "" # Nebi's Keycloak OIDC client ID jupyterhub-client-id: "" # JupyterHub's Keycloak OIDC client ID nebi-environment-selector: false # Set true to show nebi workspaces in jhub-apps env dropdown + # --------------------------------------------------------------------------- + # Profiles — resource sizing for JupyterLab pods. + # Each entry maps directly to a KubeSpawner profile_list item. + # kubespawner_override accepts any valid KubeSpawner trait (cpu_limit, + # mem_limit, node_selector, image, extra_resource_limits, etc.). + # Set to [] to disable the profile selector (single-instance mode). + profiles: + - display_name: "Small" + description: "1 CPU / 2 GB RAM" + default: true + kubespawner_override: + cpu_limit: 1 + cpu_guarantee: 0.5 + mem_limit: "2G" + mem_guarantee: "1G" + - display_name: "Medium" + description: "4 CPU / 8 GB RAM" + kubespawner_override: + cpu_limit: 4 + cpu_guarantee: 2 + mem_limit: "8G" + mem_guarantee: "4G" # Shared storage — MUST match top-level sharedStorage.* values exactly. # helm template will fail if sharedStorage.enabled and shared-storage-enabled diverge. shared-storage-enabled: false From 13aa6e76804e76466b2ea249ad5174941d9f24f0 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Sun, 29 Mar 2026 20:03:34 +0100 Subject: [PATCH 13/60] fix: add descriptive names to default server profiles Update default profile display_name and description to be more user-friendly (e.g. "Small Instance" with "Stable environment with 1 CPU / 2 GB RAM" instead of just "Small" / "1 CPU / 2 GB RAM"). --- values.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/values.yaml b/values.yaml index 736be5d..897e15b 100644 --- a/values.yaml +++ b/values.yaml @@ -143,16 +143,16 @@ jupyterhub: # mem_limit, node_selector, image, extra_resource_limits, etc.). # Set to [] to disable the profile selector (single-instance mode). profiles: - - display_name: "Small" - description: "1 CPU / 2 GB RAM" + - display_name: "Small Instance" + description: "Stable environment with 1 CPU / 2 GB RAM" default: true kubespawner_override: cpu_limit: 1 cpu_guarantee: 0.5 mem_limit: "2G" mem_guarantee: "1G" - - display_name: "Medium" - description: "4 CPU / 8 GB RAM" + - display_name: "Medium Instance" + description: "Stable environment with 4 CPU / 8 GB RAM" kubespawner_override: cpu_limit: 4 cpu_guarantee: 2 From 67a452e610b972117dd2f55d48ed78c83edea360 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Mon, 27 Apr 2026 12:04:20 +0100 Subject: [PATCH 14/60] split: move JupyterLab profiles to separate branch Profiles feature (#31) is out of scope for this PR. Moved to local branch feat/jupyterlab-profiles for a follow-up PR. --- config/jupyterhub/01-spawner.py | 15 --------------- values.yaml | 22 ---------------------- 2 files changed, 37 deletions(-) diff --git a/config/jupyterhub/01-spawner.py b/config/jupyterhub/01-spawner.py index 2191e85..7efebc9 100644 --- a/config/jupyterhub/01-spawner.py +++ b/config/jupyterhub/01-spawner.py @@ -156,21 +156,6 @@ c.KubeSpawner.environment = env -# --------------------------------------------------------------------------- -# Profiles (resource sizing) -# --------------------------------------------------------------------------- -# Each profile maps directly to a KubeSpawner profile_list entry. -# kubespawner_override accepts any valid KubeSpawner trait so deployers can -# add node_selector, image, extra_resource_limits (GPU), etc. without code changes. -# When the list is empty, no profile selector is shown (single-instance mode). -_profiles = get_config("custom.profiles", []) -if _profiles: - c.KubeSpawner.profile_list = _profiles - log.info("profiles: loaded %d profile(s): %s", len(_profiles), [p.get("display_name") for p in _profiles]) -else: - log.info("profiles: none configured — single-instance mode") - - # --------------------------------------------------------------------------- # Keycloak token exchange helpers (synchronous, shared) # --------------------------------------------------------------------------- diff --git a/values.yaml b/values.yaml index 736c61d..d20e252 100644 --- a/values.yaml +++ b/values.yaml @@ -155,28 +155,6 @@ jupyterhub: nebi-client-id: "" # Nebi's Keycloak OIDC client ID jupyterhub-client-id: "" # JupyterHub's Keycloak OIDC client ID nebi-environment-selector: false # Set true to show nebi workspaces in jhub-apps env dropdown - # --------------------------------------------------------------------------- - # Profiles — resource sizing for JupyterLab pods. - # Each entry maps directly to a KubeSpawner profile_list item. - # kubespawner_override accepts any valid KubeSpawner trait (cpu_limit, - # mem_limit, node_selector, image, extra_resource_limits, etc.). - # Set to [] to disable the profile selector (single-instance mode). - profiles: - - display_name: "Small Instance" - description: "Stable environment with 1 CPU / 2 GB RAM" - default: true - kubespawner_override: - cpu_limit: 1 - cpu_guarantee: 0.5 - mem_limit: "2G" - mem_guarantee: "1G" - - display_name: "Medium Instance" - description: "Stable environment with 4 CPU / 8 GB RAM" - kubespawner_override: - cpu_limit: 4 - cpu_guarantee: 2 - mem_limit: "8G" - mem_guarantee: "4G" # Shared storage — MUST match top-level sharedStorage.* values exactly. # helm template will fail if sharedStorage.enabled and shared-storage-enabled diverge. shared-storage-enabled: false From 38ea366e1225a9fc1aec0fa1d2db4a1c2aa89a1e Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Mon, 27 Apr 2026 12:38:46 +0100 Subject: [PATCH 15/60] test: add k3d-based e2e smoke test Replaces the inline-bash test workflow with a pytest-based suite that manages the k3d cluster, helm install, and pod-wait lifecycle. Conftest exposes a 'cluster' session fixture and a 'hub_url' fixture that port-forwards proxy-public. CI runs uvx pytest tests/e2e -v with PYTHONUNBUFFERED=1 so live logs stream into the workflow output. Locally: uvx pytest tests/e2e -v # fresh cluster K3D_CLUSTER=k3d-nebari-dev uvx pytest tests/e2e -v # reuse --- .github/workflows/test.yaml | 56 +++------------ tests/e2e/README.md | 22 ++++++ tests/e2e/conftest.py | 137 ++++++++++++++++++++++++++++++++++++ tests/e2e/pytest.ini | 5 ++ tests/e2e/test_smoke.py | 10 +++ 5 files changed, 185 insertions(+), 45 deletions(-) create mode 100644 tests/e2e/README.md create mode 100644 tests/e2e/conftest.py create mode 100644 tests/e2e/pytest.ini create mode 100644 tests/e2e/test_smoke.py diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index c454b30..b9126a9 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -7,7 +7,7 @@ on: branches: [main] jobs: - test: + e2e: runs-on: ubuntu-latest timeout-minutes: 15 @@ -18,52 +18,18 @@ jobs: - name: Install k3d run: curl -s https://raw.githubusercontent.com/k3d-io/k3d/main/install.sh | bash - - name: Create k3d cluster - run: k3d cluster create test --wait - - name: Install Helm - run: | - curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash - - - name: Update Helm dependencies - run: helm dependency update - - - name: Deploy chart - run: helm install data-science-pack . --namespace default --set nebariapp.enabled=false + run: curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash - - name: Wait for pods with progress - run: | - for i in {1..60}; do - echo "=== Attempt $i/60 ===" - kubectl get pods - echo "--- Hub logs (last 10 lines) ---" - kubectl logs -l component=hub --tail=10 2>/dev/null || echo "(not ready)" - echo "--- Proxy logs (last 5 lines) ---" - kubectl logs -l component=proxy --tail=5 2>/dev/null || echo "(not ready)" - if kubectl wait --for=condition=ready pod -l component=hub --timeout=5s 2>/dev/null && \ - kubectl wait --for=condition=ready pod -l component=proxy --timeout=5s 2>/dev/null; then - echo "All pods ready!" - exit 0 - fi - sleep 5 - done - echo "Timeout waiting for pods" - exit 1 + - name: Install uv + run: curl -LsSf https://astral.sh/uv/install.sh | sh - - name: Test JupyterHub endpoint - run: | - kubectl port-forward svc/proxy-public 8000:80 & - PF_PID=$! - sleep 5 - echo "=== Response from /hub/login ===" - curl -sS http://localhost:8000/hub/login || true - echo "" - echo "=== Checking for JupyterHub ===" - curl -sS http://localhost:8000/hub/login | grep -q "JupyterHub" - RESULT=$? - kill $PF_PID 2>/dev/null || true - exit $RESULT + - name: Run e2e tests + env: + K3D_KEEP: "1" # runner is ephemeral, skip teardown + PYTHONUNBUFFERED: "1" # stream logs live (no block buffering on non-TTY) + run: uvx pytest tests/e2e -v --color=yes - - name: Check hub logs for errors + - name: Hub logs on failure if: failure() - run: kubectl logs -l component=hub --tail=100 + run: kubectl logs -l component=hub --tail=200 || true diff --git a/tests/e2e/README.md b/tests/e2e/README.md new file mode 100644 index 0000000..ab8ba4b --- /dev/null +++ b/tests/e2e/README.md @@ -0,0 +1,22 @@ +# e2e tests + +Spins up a k3d cluster, helm-installs the chart, runs behavioral tests against the live hub. + +## Run + +```bash +# fresh cluster (creates + tears down) +uvx pytest tests/e2e -v + +# reuse an existing cluster (skip create/delete) +K3D_CLUSTER=k3d-nebari-dev uvx pytest tests/e2e -v + +# keep the cluster after the run +K3D_KEEP=1 uvx pytest tests/e2e -v +``` + +Logs stream live (configured in `tests/e2e/pytest.ini`). + +## Requirements + +`k3d`, `helm`, `kubectl` on `PATH`. diff --git a/tests/e2e/conftest.py b/tests/e2e/conftest.py new file mode 100644 index 0000000..17792b8 --- /dev/null +++ b/tests/e2e/conftest.py @@ -0,0 +1,137 @@ +"""k3d + helm fixtures for end-to-end tests. + +Reuses an existing cluster if `K3D_CLUSTER` is set (fast iteration), +otherwise creates a throwaway one named `nbtest-e2e`. +""" + +import logging +import os +import shutil +import subprocess +import time + +import pytest +import urllib.request +import urllib.error + +log = logging.getLogger(__name__) + +CLUSTER = os.environ.get("K3D_CLUSTER", "nbtest-e2e") +RELEASE = "ds" +NAMESPACE = "default" +HUB_LOCAL_PORT = 18000 + + +def _run(*args, check=True, capture=False): + """Run a subprocess. Streams stdout/stderr live via logging.""" + log.info("$ %s", " ".join(args)) + if capture: + cp = subprocess.run(args, check=check, capture_output=True, text=True) + for line in (cp.stdout or "").splitlines(): + log.info(" %s", line) + for line in (cp.stderr or "").splitlines(): + log.warning(" %s", line) + return cp + proc = subprocess.Popen(args, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, text=True, bufsize=1) + for line in proc.stdout: + log.info(" %s", line.rstrip()) + rc = proc.wait() + if check and rc != 0: + raise subprocess.CalledProcessError(rc, args) + return subprocess.CompletedProcess(args, rc) + + +def _require(binary): + if not shutil.which(binary): + pytest.exit(f"{binary} not found on PATH", returncode=2) + + +def _cluster_exists(name): + cp = _run("k3d", "cluster", "list", "-o", "json", + check=False, capture=True) + return f'"name":"{name}"' in cp.stdout + + +def _wait_for_hub(timeout_s, poll_s): + """Poll until hub + proxy pods are Ready.""" + deadline = time.time() + timeout_s + attempt = 0 + while time.time() < deadline: + attempt += 1 + elapsed = int(timeout_s - (deadline - time.time())) + log.info("hub-wait attempt=%d elapsed=%ds", attempt, elapsed) + _run("kubectl", "get", "pods", "-n", NAMESPACE, + check=False, capture=True) + _run("kubectl", "logs", "-n", NAMESPACE, "-l", "component=hub", + "--tail=5", check=False, capture=True) + hub = _run("kubectl", "wait", "--for=condition=ready", "pod", + "-l", "component=hub", "-n", NAMESPACE, + "--timeout=2s", check=False, capture=True) + proxy = _run("kubectl", "wait", "--for=condition=ready", "pod", + "-l", "component=proxy", "-n", NAMESPACE, + "--timeout=2s", check=False, capture=True) + if hub.returncode == 0 and proxy.returncode == 0: + log.info("hub+proxy ready after %ds", elapsed) + return + time.sleep(poll_s) + log.error("timeout after %ds; dumping last 200 hub log lines", timeout_s) + _run("kubectl", "logs", "-n", NAMESPACE, "-l", "component=hub", + "--tail=200", check=False, capture=True) + pytest.fail(f"hub/proxy not ready within {timeout_s}s") + + +@pytest.fixture(scope="session") +def cluster(): + for b in ("k3d", "helm", "kubectl"): + _require(b) + + created_here = False + if not _cluster_exists(CLUSTER): + log.info("creating k3d cluster %s", CLUSTER) + _run("k3d", "cluster", "create", CLUSTER, "--wait") + created_here = True + else: + log.info("reusing existing cluster %s", CLUSTER) + + _run("helm", "dependency", "update") + _run("helm", "upgrade", "--install", RELEASE, ".", + "--namespace", NAMESPACE, + "--set", "nebariapp.enabled=false") + + _wait_for_hub(timeout_s=300, poll_s=5) + + yield CLUSTER + + if created_here and not os.environ.get("K3D_KEEP"): + log.info("deleting cluster %s", CLUSTER) + _run("k3d", "cluster", "delete", CLUSTER, check=False) + + +@pytest.fixture +def hub_url(cluster): + """Port-forward proxy-public, yield base URL, tear down.""" + log.info("port-forward svc/proxy-public -> :%d", HUB_LOCAL_PORT) + pf = subprocess.Popen( + ["kubectl", "port-forward", "svc/proxy-public", + f"{HUB_LOCAL_PORT}:80", "-n", NAMESPACE], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, + ) + base = f"http://localhost:{HUB_LOCAL_PORT}" + try: + deadline = time.time() + 30 + while time.time() < deadline: + try: + with urllib.request.urlopen(f"{base}/hub/login", timeout=2) as r: + if r.status == 200: + break + except (urllib.error.URLError, ConnectionResetError): + time.sleep(0.5) + else: + pytest.fail("port-forward never became reachable") + log.info("port-forward ready at %s", base) + yield base + finally: + log.info("closing port-forward") + pf.terminate() + pf.wait(timeout=5) diff --git a/tests/e2e/pytest.ini b/tests/e2e/pytest.ini new file mode 100644 index 0000000..57144ff --- /dev/null +++ b/tests/e2e/pytest.ini @@ -0,0 +1,5 @@ +[pytest] +log_cli = true +log_cli_level = INFO +log_cli_format = %(asctime)s [%(levelname)s] %(name)s: %(message)s +log_cli_date_format = %H:%M:%S diff --git a/tests/e2e/test_smoke.py b/tests/e2e/test_smoke.py new file mode 100644 index 0000000..524aa85 --- /dev/null +++ b/tests/e2e/test_smoke.py @@ -0,0 +1,10 @@ +"""Smoke test: chart installs, hub serves /hub/login.""" + +import urllib.request + + +def test_hub_login_page(hub_url): + with urllib.request.urlopen(f"{hub_url}/hub/login", timeout=5) as r: + body = r.read().decode() + assert r.status == 200 + assert "JupyterHub" in body From e23f1b1c7f90b1bb8cf304454f38402c1a68b08d Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Mon, 27 Apr 2026 14:20:34 +0100 Subject: [PATCH 16/60] test: add NFS-backed shared-storage e2e tests Switches the e2e harness to kind (k3d's busybox-on-scratch nodes lack a package manager, so the chart's nfs-common installer DaemonSet can't provision NFS client tools). New tests in tests/e2e/test_shared_storage.py exercise the full PR #30 spawn path against a real cluster: - test_user_in_group_can_write: alice-data writes /shared/data/... - test_shared_dir_is_group_owned: dir mode is 2775 (setgid) The DummyAuth shim in tests/e2e/fixtures/test-values.yaml maps the login username to user+groups (alice-data -> alice in [data]) so we can fake auth_state without running Keycloak. Everything else (spawner hook, init container, NSS wrapper, NFS mount) is real. Chart changes: - sharedStorage.nfsServer.mountOptions added (default []). Tests pass [nfsvers=3] because kind nodes use overlayfs which fails the volume-nfs image's NFSv4 root export. Production unchanged. Conftest infrastructure: - kind cluster fixture with KIND_KEEP=1 reuse - hosts-entry workaround so kubelet's host mount.nfs can resolve the cluster-internal NFS service FQDN (kind nodes have no cluster DNS in their host resolv.conf) - structured logging + step counter + per-cycle pod state, events from kubectl describe, and node-level kubelet journal lines - autouse failure-dump fixture (kubectl get pods/events + hub and singleuser logs) --- .github/workflows/test.yaml | 8 +- templates/shared-pvc.yaml | 9 + tests/e2e/README.md | 13 +- tests/e2e/conftest.py | 470 +++++++++++++++++++++++++++- tests/e2e/fixtures/test-values.yaml | 58 ++++ tests/e2e/test_shared_storage.py | 16 + values.yaml | 5 + 7 files changed, 557 insertions(+), 22 deletions(-) create mode 100644 tests/e2e/fixtures/test-values.yaml create mode 100644 tests/e2e/test_shared_storage.py diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index b9126a9..67428c5 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -15,8 +15,10 @@ jobs: - name: Checkout uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - - name: Install k3d - run: curl -s https://raw.githubusercontent.com/k3d-io/k3d/main/install.sh | bash + - name: Install kind + run: | + curl -fsSLo /tmp/kind https://kind.sigs.k8s.io/dl/v0.27.0/kind-linux-amd64 + sudo install -m 0755 /tmp/kind /usr/local/bin/kind - name: Install Helm run: curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash @@ -26,7 +28,7 @@ jobs: - name: Run e2e tests env: - K3D_KEEP: "1" # runner is ephemeral, skip teardown + KIND_KEEP: "1" # runner is ephemeral, skip teardown PYTHONUNBUFFERED: "1" # stream logs live (no block buffering on non-TTY) run: uvx pytest tests/e2e -v --color=yes diff --git a/templates/shared-pvc.yaml b/templates/shared-pvc.yaml index 126aaa6..0439134 100644 --- a/templates/shared-pvc.yaml +++ b/templates/shared-pvc.yaml @@ -36,6 +36,15 @@ spec: accessModes: - ReadWriteMany persistentVolumeReclaimPolicy: Retain + {{- with .Values.sharedStorage.nfsServer.mountOptions }} + # Set sharedStorage.nfsServer.mountOptions=[nfsvers=3] for clusters where + # the host filesystem is overlayfs (kind, k3d, some containerd setups): + # the volume-nfs image fails to export `/` on overlayfs but leaves it in + # the export table with fsid=0, which breaks NFSv4 pseudo-fs traversal. + # On regular ext4/xfs nodes the default (NFSv4) works fine — leave empty. + mountOptions: + {{- toYaml . | nindent 4 }} + {{- end }} nfs: server: {{ include "nebari-data-science-pack.fullname" . }}-nfs.{{ .Release.Namespace }}.svc.cluster.local path: /exports diff --git a/tests/e2e/README.md b/tests/e2e/README.md index ab8ba4b..094906d 100644 --- a/tests/e2e/README.md +++ b/tests/e2e/README.md @@ -1,6 +1,11 @@ # e2e tests -Spins up a k3d cluster, helm-installs the chart, runs behavioral tests against the live hub. +Spins up a kind cluster, helm-installs the chart, runs behavioral tests against the live hub. + +kind (not k3d) — kind nodes are full ubuntu containers, so the chart's +`installClient` DaemonSet (apt-based) can install nfs-common, which is +needed for the in-cluster NFS server feature. k3d's busybox-on-scratch +nodes have no package manager. ## Run @@ -9,14 +14,14 @@ Spins up a k3d cluster, helm-installs the chart, runs behavioral tests against t uvx pytest tests/e2e -v # reuse an existing cluster (skip create/delete) -K3D_CLUSTER=k3d-nebari-dev uvx pytest tests/e2e -v +KIND_CLUSTER=my-cluster uvx pytest tests/e2e -v # keep the cluster after the run -K3D_KEEP=1 uvx pytest tests/e2e -v +KIND_KEEP=1 uvx pytest tests/e2e -v ``` Logs stream live (configured in `tests/e2e/pytest.ini`). ## Requirements -`k3d`, `helm`, `kubectl` on `PATH`. +`kind`, `helm`, `kubectl` on `PATH`. diff --git a/tests/e2e/conftest.py b/tests/e2e/conftest.py index 17792b8..7d50155 100644 --- a/tests/e2e/conftest.py +++ b/tests/e2e/conftest.py @@ -1,25 +1,34 @@ -"""k3d + helm fixtures for end-to-end tests. +"""kind + helm fixtures for end-to-end tests. -Reuses an existing cluster if `K3D_CLUSTER` is set (fast iteration), +Reuses an existing cluster if `KIND_CLUSTER` is set (fast iteration), otherwise creates a throwaway one named `nbtest-e2e`. """ +import http.cookiejar +import json import logging import os +import pathlib import shutil import subprocess import time - -import pytest +import urllib.parse import urllib.request import urllib.error +import pytest + log = logging.getLogger(__name__) -CLUSTER = os.environ.get("K3D_CLUSTER", "nbtest-e2e") + +def _step(n, total, msg): + log.info("──── [%d/%d] %s ────", n, total, msg) + +CLUSTER = os.environ.get("KIND_CLUSTER", "nbtest-e2e") RELEASE = "ds" NAMESPACE = "default" HUB_LOCAL_PORT = 18000 +TEST_VALUES = pathlib.Path(__file__).parent / "fixtures" / "test-values.yaml" def _run(*args, check=True, capture=False): @@ -48,9 +57,8 @@ def _require(binary): def _cluster_exists(name): - cp = _run("k3d", "cluster", "list", "-o", "json", - check=False, capture=True) - return f'"name":"{name}"' in cp.stdout + cp = _run("kind", "get", "clusters", check=False, capture=True) + return name in (cp.stdout or "").splitlines() def _wait_for_hub(timeout_s, poll_s): @@ -81,31 +89,156 @@ def _wait_for_hub(timeout_s, poll_s): pytest.fail(f"hub/proxy not ready within {timeout_s}s") +@pytest.hookimpl(tryfirst=True, hookwrapper=True) +def pytest_runtest_makereport(item, call): + """Stash test outcome on the item so fixtures can detect failures.""" + outcome = yield + rep = outcome.get_result() + setattr(item, f"rep_{rep.when}", rep) + + +@pytest.fixture(autouse=True) +def dump_diagnostics_on_failure(request): + """If the test fails, dump hub logs + pods + events for debugging.""" + yield + rep = getattr(request.node, "rep_call", None) + if not (rep and rep.failed): + return + log.error("=" * 60) + log.error("test failed: %s — dumping cluster diagnostics", request.node.name) + log.error("=" * 60) + _run("kubectl", "get", "pods", "-n", NAMESPACE, + check=False, capture=True) + _run("kubectl", "get", "events", "-n", NAMESPACE, + "--sort-by=.lastTimestamp", check=False, capture=True) + log.error("--- hub logs (tail 200) ---") + _run("kubectl", "logs", "-n", NAMESPACE, "-l", "component=hub", + "--tail=200", check=False, capture=True) + log.error("--- singleuser pod logs (any user) ---") + _run("kubectl", "logs", "-n", NAMESPACE, + "-l", "component=singleuser-server", "--tail=100", + "--all-containers=true", "--prefix=true", + check=False, capture=True) + + +def _patch_nfs_pv_to_cluster_ip(): + """Add the NFS service FQDN to the kind node's /etc/hosts. + + kind nodes' host /etc/resolv.conf only knows Docker DNS, so `mount.nfs` + (which runs in the host mount namespace) can't resolve cluster-internal + FQDNs. Patching the PV's `nfs.server` to the IP would work but is + rejected as immutable once the PV is Bound. Adding a hosts entry on + each kind node bypasses DNS entirely and works post-bind. + Production clusters don't need this — their kubelets have cluster DNS. + """ + rc, pv_name, _ = _kctl( + "get", "pv", "-l", + f"app.kubernetes.io/instance={RELEASE}", + "-o", "jsonpath={.items[?(@.spec.nfs)].metadata.name}", + ) + pv_name = pv_name.strip() + if not pv_name: + log.info("no NFS-backed PV found — skipping hosts entry") + return + + rc, fqdn, _ = _kctl( + "get", "pv", pv_name, + "-o", "jsonpath={.spec.nfs.server}", + ) + fqdn = fqdn.strip() + svc_name = fqdn.split(".", 1)[0] + rc, svc_ip, err = _kctl( + "get", "svc", svc_name, + "-o", "jsonpath={.spec.clusterIP}", + ) + svc_ip = svc_ip.strip() + if not svc_ip: + log.error("could not resolve NFS svc %s ClusterIP: %s", svc_name, err) + return + + # Add to each kind node's /etc/hosts (idempotent: skip if already there). + cp = subprocess.run( + ["kind", "get", "nodes", "--name", CLUSTER], + capture_output=True, text=True, check=True, + ) + for node in cp.stdout.strip().splitlines(): + cmd = (f"grep -q '{fqdn}$' /etc/hosts || " + f"echo '{svc_ip} {fqdn}' >> /etc/hosts") + log.info("hosts entry on %s: %s -> %s", node, fqdn, svc_ip) + subprocess.run(["docker", "exec", node, "sh", "-c", cmd], check=True) + + @pytest.fixture(scope="session") def cluster(): - for b in ("k3d", "helm", "kubectl"): + TOTAL = 7 + _step(1, TOTAL, "verify required binaries on PATH") + for b in ("kind", "helm", "kubectl"): _require(b) created_here = False if not _cluster_exists(CLUSTER): - log.info("creating k3d cluster %s", CLUSTER) - _run("k3d", "cluster", "create", CLUSTER, "--wait") + _step(2, TOTAL, f"create kind cluster '{CLUSTER}'") + _run("kind", "create", "cluster", "--name", CLUSTER, "--wait", "60s") created_here = True else: - log.info("reusing existing cluster %s", CLUSTER) + _step(2, TOTAL, f"reuse existing kind cluster '{CLUSTER}'") + # `kind delete` can wipe the kubeconfig entry while leaving the + # container; re-export to make sure the context is wired up. + _run("kind", "export", "kubeconfig", "--name", CLUSTER, + check=True, capture=True) + _step(3, TOTAL, "helm dependency update") _run("helm", "dependency", "update") + + _step(4, TOTAL, "helm install (chart + test overrides)") + _t = time.time() _run("helm", "upgrade", "--install", RELEASE, ".", "--namespace", NAMESPACE, - "--set", "nebariapp.enabled=false") + "--set", "nebariapp.enabled=false", + "--values", str(TEST_VALUES)) + log.info("helm install completed in %ds", int(time.time() - _t)) + + _step(5, TOTAL, "patch NFS PV server field to ClusterIP (kind workaround)") + _patch_nfs_pv_to_cluster_ip() + _step(6, TOTAL, "snapshot post-install resources") + _run("kubectl", "get", "all,pvc,configmap", "-n", NAMESPACE, + check=False, capture=True) + + _step(7, TOTAL, "wait for hub + proxy ready") _wait_for_hub(timeout_s=300, poll_s=5) yield CLUSTER - if created_here and not os.environ.get("K3D_KEEP"): + if created_here and not os.environ.get("KIND_KEEP"): log.info("deleting cluster %s", CLUSTER) - _run("k3d", "cluster", "delete", CLUSTER, check=False) + cp = _run("kind", "delete", "cluster", "--name", CLUSTER, check=False, + capture=True) + if cp.returncode != 0: + # Docker Desktop occasionally fails to kill a container with + # active mounts. Force-remove orphan node containers so the next + # run starts clean. + log.warning("kind delete failed; force-removing node containers") + cp2 = subprocess.run( + ["docker", "ps", "-a", "--filter", + f"name={CLUSTER}-", "--format", "{{.Names}}"], + capture_output=True, text=True, + ) + for name in (cp2.stdout or "").strip().splitlines(): + # `docker stop` first with timeout=0 (SIGKILL immediately), + # then rm -f. Avoids the "did not receive an exit event" hang. + subprocess.run( + ["docker", "stop", "--time=0", name], + capture_output=True, + ) + subprocess.run( + ["docker", "rm", "-f", "-v", name], + capture_output=True, + ) + subprocess.run( + ["docker", "network", "rm", f"kind"], + capture_output=True, + ) @pytest.fixture @@ -135,3 +268,310 @@ def hub_url(cluster): log.info("closing port-forward") pf.terminate() pf.wait(timeout=5) + + +class SpawnedUser: + """Handle to a logged-in JupyterHub user with a running singleuser pod.""" + + def __init__(self, login_name, real_user, pod): + self.login_name = login_name # e.g. "alice-data" (sent to /hub/login) + self.user = real_user # e.g. "alice" (authenticator-resolved) + self.pod = pod # k8s pod name + + def exec(self, *cmd, user=None): + """Run a command inside the singleuser pod, return (rc, stdout).""" + flags = ["-n", NAMESPACE, self.pod, "-c", "notebook", "--"] + if user: + return _kexec(*flags, "su", "-", user, "-c", " ".join(cmd)) + return _kexec(*flags, *cmd) + + +def _kexec(*args): + cp = subprocess.run( + ["kubectl", "exec", *args], + capture_output=True, text=True, + ) + # Concatenate stderr after stdout for diagnostics on failure but the + # primary signal (return code, stdout) is what the test asserts on. + out = (cp.stdout or "") + (cp.stderr or "") + return cp.returncode, out + + +def _login_and_spawn(base, login_name, timeout_s=180): + """POST /hub/login + start server + wait for pod ready. Returns SpawnedUser.""" + SPAWN_TOTAL = 4 + jar = http.cookiejar.CookieJar() + opener = urllib.request.build_opener( + urllib.request.HTTPCookieProcessor(jar), + ) + + def _xsrf(): + for c in jar: + if c.name == "_xsrf": + return c.value + return "" + + def _request(method, path, data=None, headers=None): + url = base + path + body = urllib.parse.urlencode(data).encode() if data else None + req = urllib.request.Request(url, data=body, method=method, + headers=headers or {}) + try: + r = opener.open(req, timeout=15) + payload = r.read().decode(errors="replace") + return r.status, dict(r.headers), payload + except urllib.error.HTTPError as e: + payload = e.read().decode(errors="replace") if e.fp else "" + return e.code, dict(e.headers or {}), payload + + # 1. Prime _xsrf cookie + _step(1, SPAWN_TOTAL, "GET /hub/login (prime _xsrf cookie)") + status, _, _ = _request("GET", "/hub/login") + log.info(" status=%d cookies=%s", status, [c.name for c in jar]) + + # 2. Login (DummyAuthenticator accepts any password). Retry on 5xx — the + # hub briefly returns 503 right after a previous user pod is deleted + # while it cleans up the spawner state. + _step(2, SPAWN_TOTAL, f"POST /hub/login (as {login_name})") + deadline = time.time() + 30 + while True: + status, _, body = _request( + "POST", "/hub/login", + data={"username": login_name, "password": "x", "_xsrf": _xsrf()}, + ) + log.info(" status=%d body[:200]=%r", status, body[:200]) + if status in (200, 302): + break + if status >= 500 and time.time() < deadline: + log.info(" retrying after 5xx in 2s") + time.sleep(2) + continue + pytest.fail(f"login failed status={status}: {body}") + + # 3. Start the server + real_user = login_name.split("-")[0] + _step(3, SPAWN_TOTAL, f"POST /hub/api/users/{real_user}/server") + status, headers, body = _request( + "POST", f"/hub/api/users/{real_user}/server", + headers={"X-XSRFToken": _xsrf()}, + ) + log.info(" status=%d body=%s", status, body) + if status not in (201, 202, 400): # 400 = already running + log.error("spawn POST returned %d", status) + log.error(" response headers: %s", headers) + log.error(" response body: %s", body) + log.error(" request cookies: %s", [c.name for c in jar]) + pytest.fail(f"spawn POST returned {status}: {body}") + + # 4. Wait for pod ready + pod_label = f"hub.jupyter.org/username={real_user}" + _step(4, SPAWN_TOTAL, f"wait for pod with label {pod_label}") + deadline = time.time() + timeout_s + pod_name = None + while time.time() < deadline: + cp = _run("kubectl", "get", "pods", "-n", NAMESPACE, + "-l", pod_label, "-o", "jsonpath={.items[0].metadata.name}", + check=False, capture=True) + if cp.returncode == 0 and cp.stdout.strip(): + pod_name = cp.stdout.strip() + break + time.sleep(2) + if not pod_name: + pytest.fail(f"pod for user {real_user} never appeared") + + _wait_for_pod_ready(pod_name, timeout_s=timeout_s, poll_s=5) + return SpawnedUser(login_name, real_user, pod_name) + + +_INIT_CONTAINERS = ("block-cloud-metadata", "initialize-shared-mounts") + + +def _kctl(*args, timeout=10): + """Quiet kubectl helper that returns trimmed stdout (no live logging).""" + cp = subprocess.run(["kubectl", *args, "-n", NAMESPACE], + capture_output=True, text=True, timeout=timeout) + return cp.returncode, (cp.stdout or "").strip(), (cp.stderr or "").strip() + + +def _wait_for_pod_ready(pod_name, timeout_s, poll_s): + """Poll pod state until Ready, surfacing every observable kubelet signal. + + Each cycle logs: + - one-line pod summary (phase, container/init readiness) + - new events since the last poll (image pull, FailedMount, scheduling…) + - nfs-client-installer DaemonSet phase (PR #30 prereq for NFS) + - tail of any init/notebook container logs that have started + """ + deadline = time.time() + timeout_s + attempt = 0 + seen_events: set[str] = set() + seen_log_lines: dict[str, set[str]] = {c: set() for c in _INIT_CONTAINERS} + while time.time() < deadline: + attempt += 1 + elapsed = int(timeout_s - (deadline - time.time())) + log.info("=== pod-wait %s attempt=%d elapsed=%ds ===", + pod_name, attempt, elapsed) + + # 1. Pod summary + rc, out, _ = _kctl("get", "pod", pod_name, "--no-headers") + if rc == 0: + log.info(" pod: %s", out) + else: + log.warning(" pod not found yet") + + # 2. Per-container state (more readable than jsonpath blob) + rc, out, _ = _kctl( + "get", "pod", pod_name, "-o", + "jsonpath={range .status.initContainerStatuses[*]}" + "init/{.name} ready={.ready} state={.state}{'\\n'}{end}" + "{range .status.containerStatuses[*]}" + "main/{.name} ready={.ready} state={.state}{'\\n'}{end}", + ) + for line in out.splitlines(): + log.info(" %s", line) + + # 3. Pod events from kubectl describe (canonical, with aggregation). + # Dedup on (Type, Reason, Message) — drop the Age column which + # keeps changing and breaks naive string-equality dedup. + rc, out, _ = _kctl("describe", "pod", pod_name, timeout=10) + in_events = False + for raw in out.splitlines(): + stripped = raw.strip() + if raw.startswith("Events:"): + in_events = True; continue + if raw and not raw.startswith(" "): + in_events = False + if not (in_events and stripped): + continue + if stripped.startswith("Type") or stripped.startswith("----"): + continue + # describe-pod event row: "Type Reason Age From Message" + # Drop column index 2 (Age) for stable dedup. + cols = stripped.split(None, 4) + key = (cols[0], cols[1], cols[3] if len(cols) > 3 else "", + cols[4] if len(cols) > 4 else "") + if key in seen_events: + continue + seen_events.add(key) + log.info(" event: %s", stripped) + + # 4. Node-level kubelet logs (the deepest signal: NFS mount attempts, + # image pull progress, container exec). kind nodes are docker + # containers; journalctl is available inside. + rc, out, _ = _kctl( + "get", "pod", pod_name, "-o", "jsonpath={.spec.nodeName}", + ) + if out: + node = out.strip() + cp = subprocess.run( + ["docker", "exec", node, "journalctl", "-u", "kubelet", + "--since", "8 seconds ago", "--no-pager", "-q", + "-o", "cat"], + capture_output=True, text=True, timeout=5, + ) + for line in (cp.stdout or "").splitlines(): + # Filter to lines that mention this pod's name or its uid. + if pod_name in line or "FailedMount" in line or "MountVolume" in line: + if line in seen_events: + continue + seen_events.add(line) + log.info(" kubelet: %s", line) + + # 4. NFS client installer DaemonSet status + init-container logs. + # The "1/1 Running" we see is the pause container — the actual + # apt-install runs in an init container; failures hide unless + # we tail its logs. + rc, out, _ = _kctl( + "get", "pods", "-l", + "app.kubernetes.io/component=nfs-client-installer", + "--no-headers", + ) + if out: + log.info(" nfs-installer: %s", out) + for installer_pod in (line.split()[0] for line in out.splitlines()): + rc2, init_log, _ = _kctl( + "logs", installer_pod, "-c", "install-nfs-common", + "--tail=20", timeout=5, + ) + if rc2 == 0: + for line in (init_log or "").splitlines(): + key = ("installer-log", line) + if key in seen_events: + continue + seen_events.add(key) + log.info(" installer: %s", line) + + # 5. Init + main container logs as they start producing output. + for c in _INIT_CONTAINERS + ("notebook",): + rc, out, _ = _kctl( + "logs", pod_name, "-c", c, "--tail=20", timeout=5, + ) + if rc != 0: + continue + for line in out.splitlines(): + if line in seen_log_lines.setdefault(c, set()): + continue + seen_log_lines[c].add(line) + log.info(" %s: %s", c, line) + + # Ready? + ready = subprocess.run( + ["kubectl", "wait", "--for=condition=ready", "pod", pod_name, + "-n", NAMESPACE, "--timeout=2s"], + capture_output=True, text=True, + ) + if ready.returncode == 0: + log.info("pod %s ready after %ds", pod_name, elapsed) + return + time.sleep(poll_s) + pytest.fail(f"pod {pod_name} not ready within {timeout_s}s") + + +@pytest.fixture +def spawn_user(hub_url): + """Factory: login and start a singleuser pod for a username convention. + + Username 'alice-data-ml' -> User(name='alice', groups=['data','ml']). + Pods are stopped and deleted in teardown. + """ + spawned: list[SpawnedUser] = [] + + def _spawn(login_name): + u = _login_and_spawn(hub_url, login_name) + spawned.append(u) + return u + + yield _spawn + + # Stop via the JupyterHub API so the spawner state is cleaned up (a + # raw pod delete leaves the hub thinking the server is still pending, + # causing 503s on the next login). + for u in spawned: + log.info("stopping server for %s via /hub/api", u.user) + _stop_server(hub_url, u.login_name, u.user) + + +def _stop_server(base, login_name, real_user): + """DELETE /hub/api/users//server (login first to get auth cookie).""" + jar = http.cookiejar.CookieJar() + opener = urllib.request.build_opener( + urllib.request.HTTPCookieProcessor(jar)) + try: + opener.open(base + "/hub/login", timeout=10).read() + xsrf = next((c.value for c in jar if c.name == "_xsrf"), "") + opener.open(urllib.request.Request( + base + "/hub/login", method="POST", + data=urllib.parse.urlencode( + {"username": login_name, "password": "x", "_xsrf": xsrf} + ).encode(), + ), timeout=10).read() + xsrf = next((c.value for c in jar if c.name == "_xsrf"), "") + opener.open(urllib.request.Request( + base + f"/hub/api/users/{real_user}/server", + method="DELETE", + headers={"X-XSRFToken": xsrf}, + ), timeout=15).read() + except urllib.error.HTTPError as e: + log.warning("stop_server: %s -> %d", real_user, e.code) + except Exception as e: + log.warning("stop_server failed: %s", e) diff --git a/tests/e2e/fixtures/test-values.yaml b/tests/e2e/fixtures/test-values.yaml new file mode 100644 index 0000000..b4388d9 --- /dev/null +++ b/tests/e2e/fixtures/test-values.yaml @@ -0,0 +1,58 @@ +# Test overrides for kind-based e2e suite. +# +# - sharedStorage with in-cluster NFS server (works on kind because kind +# nodes are full ubuntu containers and the installClient DaemonSet can +# apt-install nfs-common). +# - DummyAuthenticator subclass that derives groups from the username: +# login as "alice-data-ml" -> User(name="alice", auth_state.groups=["data","ml"]) +# Lets feature tests inject group membership without running Keycloak. + +sharedStorage: + enabled: true + size: 1Gi + nfsServer: + enabled: true + installClient: true + storageClass: standard + # kind nodes are overlayfs — force NFSv3 (see chart docs). + mountOptions: + - nfsvers=3 + +jupyterhub: + custom: + shared-storage-enabled: true + shared-storage-mount-prefix: "/shared" + + hub: + config: + Authenticator: + admin_users: + - admin + # auth_state encryption is required to persist groups across requests + enable_auth_state: true + + # CryptKeeper.keys expects bytes; YAML can't express that directly. + # Pass as hex via env — JupyterHub decodes JUPYTERHUB_CRYPT_KEY itself. + extraEnv: + JUPYTERHUB_CRYPT_KEY: "0000000000000000000000000000000000000000000000000000000000000000" + + extraConfig: + 00-test-auth.py: | + from jupyterhub.auth import DummyAuthenticator + + class TestAuth(DummyAuthenticator): + """Maps username 'alice-data-ml' to user 'alice' with groups [data, ml].""" + + async def authenticate(self, handler, data): + username = data["username"] + parts = username.split("-") + return { + "name": parts[0], + "auth_state": {"groups": parts[1:]}, + } + + async def refresh_user(self, user, handler=None): + state = await user.get_auth_state() or {} + return {"name": user.name, "auth_state": state} + + c.JupyterHub.authenticator_class = TestAuth diff --git a/tests/e2e/test_shared_storage.py b/tests/e2e/test_shared_storage.py new file mode 100644 index 0000000..36acf89 --- /dev/null +++ b/tests/e2e/test_shared_storage.py @@ -0,0 +1,16 @@ +"""Behavior tests for PR #30 shared-storage feature.""" + + +def test_user_in_group_can_write(spawn_user): + """alice (in group 'data') can write to /shared/data/.""" + u = spawn_user("alice-data") + rc, out = u.exec("touch", "/shared/data/file_from_alice") + assert rc == 0, f"expected write to succeed, got rc={rc}: {out}" + + +def test_shared_dir_is_group_owned(spawn_user): + """/shared/data has gid matching the 'data' group and mode 2775 (setgid).""" + u = spawn_user("alice-data") + rc, out = u.exec("stat", "-c", "%a", "/shared/data") + assert rc == 0 + assert out.strip() == "2775", f"expected mode 2775, got {out.strip()!r}" diff --git a/values.yaml b/values.yaml index d20e252..559b705 100644 --- a/values.yaml +++ b/values.yaml @@ -110,6 +110,11 @@ sharedStorage: nodeSelector: {} # Advanced: full nodeAffinity spec (overrides nodeSelector if both set) nodeAffinity: {} + # NFS PV mountOptions. Default (empty) lets kubelet negotiate NFSv4. + # Set to ["nfsvers=3"] when running on overlayfs nodes (kind, k3d, + # some containerd setups) where the volume-nfs image's NFSv4 export + # of `/` is broken — see shared-pvc.yaml for details. + mountOptions: [] # ============================================================================= # Nebi Integration From 34564c9f9ed6f01230f467a5ccaccf7dc3a94f54 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Mon, 27 Apr 2026 14:33:47 +0100 Subject: [PATCH 17/60] refactor(tests/e2e): split conftest into deep modules Conftest had grown to 577 lines mixing five concerns. Extracted into focused modules each with a small interface and large hidden impl: _process.py subprocess + kubectl helpers + step counter _hub.py HubClient (cookie/login/spawn/stop session) _pod_observer.py wait_for_pod_ready + dedup'd pod-state polling _cluster.py kind lifecycle + helm install + NFS hosts workaround conftest.py shrinks to 218 lines holding only fixtures that compose the modules. Eliminates duplication of: - cookie-jar login flow (was repeated in _login_and_spawn + _stop_server) - two parallel subprocess wrappers (_run + _kctl) - inline pod-state polling loop in the spawn flow Tests still pass locally (3 passed in 35s, cluster reused). --- tests/e2e/_cluster.py | 147 +++++++++ tests/e2e/_hub.py | 101 +++++++ tests/e2e/_pod_observer.py | 180 +++++++++++ tests/e2e/_process.py | 64 ++++ tests/e2e/conftest.py | 597 ++++++++----------------------------- 5 files changed, 611 insertions(+), 478 deletions(-) create mode 100644 tests/e2e/_cluster.py create mode 100644 tests/e2e/_hub.py create mode 100644 tests/e2e/_pod_observer.py create mode 100644 tests/e2e/_process.py diff --git a/tests/e2e/_cluster.py b/tests/e2e/_cluster.py new file mode 100644 index 0000000..afcbe45 --- /dev/null +++ b/tests/e2e/_cluster.py @@ -0,0 +1,147 @@ +"""kind cluster + chart lifecycle for the e2e harness. + +Hides: + - kind CLI invocations + kubeconfig re-export when reusing a cluster + - helm-install timing + - kind-specific NFS DNS workaround (host /etc/hosts hack) + - hub+proxy readiness wait + - force-cleanup fallback when `kind delete` fails (Docker Desktop + occasionally hangs on container removal). +""" + +import logging +import os +import shutil +import subprocess +import time + +import pytest + +from _process import NAMESPACE, kctl, kctl_out, run, step + +log = logging.getLogger("e2e") + + +def require_binaries(*names): + for n in names: + if not shutil.which(n): + pytest.exit(f"{n} not found on PATH", returncode=2) + + +def cluster_exists(name): + cp = run("kind", "get", "clusters", check=False, quiet=True) + return name in (cp.stdout or "").splitlines() + + +def ensure_cluster(name): + """Create a kind cluster or attach to an existing one. Returns True if + we created it (caller is responsible for teardown).""" + if cluster_exists(name): + # `kind delete` can wipe the kubeconfig entry but leave the + # container — re-export so kubectl/helm can reach the cluster. + run("kind", "export", "kubeconfig", "--name", name, quiet=True) + return False + run("kind", "create", "cluster", "--name", name, "--wait", "60s") + return True + + +def teardown_cluster(name): + """Best-effort delete with force-cleanup fallback. + + Docker Desktop occasionally fails to kill a container with active + mounts ("did not receive an exit event"). We force-stop+rm orphan + node containers so the next run starts clean. + """ + cp = run("kind", "delete", "cluster", "--name", name, + check=False, quiet=True) + if cp.returncode == 0: + return + log.warning("kind delete failed; force-removing node containers") + cp2 = subprocess.run( + ["docker", "ps", "-a", "--filter", f"name={name}-", + "--format", "{{.Names}}"], + capture_output=True, text=True, + ) + for container in (cp2.stdout or "").strip().splitlines(): + subprocess.run(["docker", "stop", "--time=0", container], + capture_output=True) + subprocess.run(["docker", "rm", "-f", "-v", container], + capture_output=True) + subprocess.run(["docker", "network", "rm", "kind"], + capture_output=True) + + +def helm_install(release, chart_dir, values_file): + run("helm", "dependency", "update") + t0 = time.time() + run("helm", "upgrade", "--install", release, chart_dir, + "--namespace", NAMESPACE, + "--set", "nebariapp.enabled=false", + "--values", str(values_file)) + log.info("helm install completed in %ds", int(time.time() - t0)) + + +def patch_nfs_hosts_entry(release, cluster_name): + """Append ` ` to each kind node's + /etc/hosts. + + kubelet's mount.nfs runs in the host mount namespace using the host's + /etc/resolv.conf, which on kind only knows Docker DNS — cluster-internal + FQDNs don't resolve. Patching the PV's `nfs.server` to the IP would + work but is rejected as immutable once the PV is Bound. Hosts entry + is post-bind, idempotent, and bypasses DNS entirely. + + Production clusters don't need this — their kubelets have cluster DNS. + """ + rc, pv_name, _ = kctl_out( + "get", "pv", "-l", f"app.kubernetes.io/instance={release}", + "-o", "jsonpath={.items[?(@.spec.nfs)].metadata.name}", + ) + if not pv_name: + log.info("no NFS-backed PV found — skipping hosts entry") + return + + _, fqdn, _ = kctl_out("get", "pv", pv_name, + "-o", "jsonpath={.spec.nfs.server}") + svc_name = fqdn.split(".", 1)[0] + _, svc_ip, err = kctl_out("get", "svc", svc_name, + "-o", "jsonpath={.spec.clusterIP}") + if not svc_ip: + log.error("could not resolve NFS svc %s ClusterIP: %s", svc_name, err) + return + + cp = subprocess.run( + ["kind", "get", "nodes", "--name", cluster_name], + capture_output=True, text=True, check=True, + ) + for node in cp.stdout.strip().splitlines(): + cmd = (f"grep -q '{fqdn}$' /etc/hosts || " + f"echo '{svc_ip} {fqdn}' >> /etc/hosts") + log.info("hosts entry on %s: %s -> %s", node, fqdn, svc_ip) + subprocess.run(["docker", "exec", node, "sh", "-c", cmd], check=True) + + +def wait_for_hub(timeout_s=300, poll_s=5): + """Poll until JupyterHub's hub + proxy pods are Ready.""" + deadline = time.time() + timeout_s + attempt = 0 + while time.time() < deadline: + attempt += 1 + elapsed = int(timeout_s - (deadline - time.time())) + log.info("hub-wait attempt=%d elapsed=%ds", attempt, elapsed) + kctl("get", "pods") + kctl("logs", "-l", "component=hub", "--tail=5", check=False) + if _component_ready("hub") and _component_ready("proxy"): + log.info("hub+proxy ready after %ds", elapsed) + return + time.sleep(poll_s) + log.error("timeout after %ds; dumping last 200 hub log lines", timeout_s) + kctl("logs", "-l", "component=hub", "--tail=200", check=False) + pytest.fail(f"hub/proxy not ready within {timeout_s}s") + + +def _component_ready(component): + cp = run("kubectl", "wait", "--for=condition=ready", "pod", + "-l", f"component={component}", "-n", NAMESPACE, + "--timeout=2s", check=False, quiet=True) + return cp.returncode == 0 diff --git a/tests/e2e/_hub.py b/tests/e2e/_hub.py new file mode 100644 index 0000000..ce76627 --- /dev/null +++ b/tests/e2e/_hub.py @@ -0,0 +1,101 @@ +"""HTTP client for JupyterHub against the test deployment. + +The test harness needs to: log a user in, start their server, wait for +the pod, then stop the server cleanly. All four steps share cookie/XSRF +state, which is fiddly to manage with bare urllib. + +`HubClient` owns one cookie session for the whole test and exposes a +small API. Internals (cookie jar, XSRF rotation, retry on 5xx, error +decoding) are hidden. +""" + +import http.cookiejar +import logging +import time +import urllib.error +import urllib.parse +import urllib.request + +import pytest + +log = logging.getLogger("e2e") + + +class HubClient: + """One per test. Holds cookie state across login → spawn → stop.""" + + def __init__(self, base_url): + self.base = base_url.rstrip("/") + self._jar = http.cookiejar.CookieJar() + self._opener = urllib.request.build_opener( + urllib.request.HTTPCookieProcessor(self._jar), + ) + + # ---- public API ---- + + def login(self, username, password="x", retry_5xx_for=30): + """POST /hub/login. Retries on 5xx (hub may be briefly unavailable + right after a previous user pod is deleted).""" + self._get("/hub/login") # prime _xsrf cookie + deadline = time.time() + retry_5xx_for + while True: + status, _, body = self._post( + "/hub/login", + data={"username": username, "password": password, + "_xsrf": self._xsrf()}, + ) + log.info(" login status=%d body[:120]=%r", status, body[:120]) + if status in (200, 302): + return + if status >= 500 and time.time() < deadline: + log.info(" hub 5xx — retry in 2s") + time.sleep(2) + continue + pytest.fail(f"login failed status={status}: {body}") + + def spawn(self, username): + """POST /hub/api/users//server. Returns nothing — caller waits + for the pod independently. 400 means already running (idempotent).""" + status, _, body = self._post( + f"/hub/api/users/{username}/server", + headers={"X-XSRFToken": self._xsrf()}, + ) + log.info(" spawn status=%d body=%s", status, body) + if status not in (201, 202, 400): + pytest.fail(f"spawn failed status={status}: {body}") + + def stop(self, username): + """DELETE /hub/api/users//server. Cleans up spawner state in the + hub (a raw `kubectl delete pod` leaves the spawner pending and the + next login returns 503).""" + try: + self._request( + "DELETE", f"/hub/api/users/{username}/server", + headers={"X-XSRFToken": self._xsrf()}, + ) + except urllib.error.HTTPError as e: + log.warning("stop %s -> %d", username, e.code) + + # ---- internals ---- + + def _xsrf(self): + return next((c.value for c in self._jar if c.name == "_xsrf"), "") + + def _get(self, path): + return self._request("GET", path) + + def _post(self, path, data=None, headers=None): + return self._request("POST", path, data=data, headers=headers) + + def _request(self, method, path, data=None, headers=None): + body = urllib.parse.urlencode(data).encode() if data else None + req = urllib.request.Request( + self.base + path, data=body, method=method, + headers=headers or {}, + ) + try: + r = self._opener.open(req, timeout=15) + return r.status, dict(r.headers), r.read().decode(errors="replace") + except urllib.error.HTTPError as e: + payload = e.read().decode(errors="replace") if e.fp else "" + return e.code, dict(e.headers or {}), payload diff --git a/tests/e2e/_pod_observer.py b/tests/e2e/_pod_observer.py new file mode 100644 index 0000000..d73c79f --- /dev/null +++ b/tests/e2e/_pod_observer.py @@ -0,0 +1,180 @@ +"""Live observability for a kubelet-managed pod. + +`wait_for_pod_ready(name)` polls until the pod is Ready, surfacing every +observable signal as it changes: + - one-line pod state (phase, container/init readiness) + - kubectl-describe events (deduped by Type/Reason/Source/Message) + - node-level kubelet journal lines that mention the pod + - nfs-client-installer DaemonSet status + apt-install init logs + - init/main container logs as soon as they produce output + +Dedup means new info appears each cycle; a stuck pod produces a quiet +loop with the most recent state visible at a glance. +""" + +import logging +import subprocess +import time + +import pytest + +from _process import kctl_out + +log = logging.getLogger("e2e") + + +# Init containers we expect: block-cloud-metadata is z2jh's, the rest +# come from PR #30. Names are stable across runs. +_INIT_CONTAINERS = ("block-cloud-metadata", "initialize-shared-mounts") +_LOG_CONTAINERS = _INIT_CONTAINERS + ("notebook",) + + +def wait_for_pod_ready(pod_name, timeout_s=180, poll_s=5): + """Poll until pod is Ready, with deep observability each cycle.""" + obs = _PodObserver(pod_name) + deadline = time.time() + timeout_s + attempt = 0 + while time.time() < deadline: + attempt += 1 + elapsed = int(timeout_s - (deadline - time.time())) + log.info("=== pod-wait %s attempt=%d elapsed=%ds ===", + pod_name, attempt, elapsed) + obs.snapshot() + if obs.is_ready(): + log.info("pod %s ready after %ds", pod_name, elapsed) + return + time.sleep(poll_s) + pytest.fail(f"pod {pod_name} not ready within {timeout_s}s") + + +class _PodObserver: + """Per-cycle pod snapshot with cross-cycle dedup of events + log lines.""" + + def __init__(self, pod_name): + self.pod = pod_name + self._seen = set() # dedup key for events, kubelet lines, installer logs + self._seen_logs = {c: set() for c in _LOG_CONTAINERS} + + def snapshot(self): + self._log_pod_summary() + self._log_container_states() + self._log_pod_events() + self._log_kubelet_journal() + self._log_nfs_installer() + self._log_container_outputs() + + def is_ready(self): + cp = subprocess.run( + ["kubectl", "wait", "--for=condition=ready", "pod", self.pod, + "-n", "default", "--timeout=2s"], + capture_output=True, text=True, + ) + return cp.returncode == 0 + + # ---- per-section collectors ---- + + def _log_pod_summary(self): + rc, out, _ = kctl_out("get", "pod", self.pod, "--no-headers") + if rc == 0: + log.info(" pod: %s", out) + else: + log.warning(" pod not found yet") + + def _log_container_states(self): + _, out, _ = kctl_out( + "get", "pod", self.pod, "-o", + "jsonpath=" + "{range .status.initContainerStatuses[*]}" + "init/{.name} ready={.ready} state={.state}{'\\n'}{end}" + "{range .status.containerStatuses[*]}" + "main/{.name} ready={.ready} state={.state}{'\\n'}{end}", + ) + for line in out.splitlines(): + log.info(" %s", line) + + def _log_pod_events(self): + """Parse the Events section of `kubectl describe pod`. Dedup on + (Type, Reason, Source, Message) — drop the Age column which keeps + ticking and breaks naive string-equality dedup.""" + _, out, _ = kctl_out("describe", "pod", self.pod) + in_events = False + for raw in out.splitlines(): + line = raw.strip() + if raw.startswith("Events:"): + in_events = True + continue + if raw and not raw.startswith(" "): + in_events = False + if not (in_events and line): + continue + if line.startswith(("Type", "----")): + continue + cols = line.split(None, 4) # Type Reason Age From Message + key = ("evt", cols[0], cols[1], + cols[3] if len(cols) > 3 else "", + cols[4] if len(cols) > 4 else "") + if key not in self._seen: + self._seen.add(key) + log.info(" event: %s", line) + + def _log_kubelet_journal(self): + """Tail kubelet's systemd journal on the pod's node, filtered to + lines that mention this pod (the deepest observable signal — shows + actual mount.nfs / image-pull / runtime-exec activity).""" + _, node, _ = kctl_out( + "get", "pod", self.pod, "-o", "jsonpath={.spec.nodeName}", + ) + if not node: + return + cp = subprocess.run( + ["docker", "exec", node, "journalctl", "-u", "kubelet", + "--since", "8 seconds ago", "--no-pager", "-q", "-o", "cat"], + capture_output=True, text=True, timeout=5, + ) + for line in (cp.stdout or "").splitlines(): + if not (self.pod in line + or "FailedMount" in line + or "MountVolume" in line): + continue + key = ("kubelet", line) + if key not in self._seen: + self._seen.add(key) + log.info(" kubelet: %s", line) + + def _log_nfs_installer(self): + """The chart's nfs-client-installer DaemonSet apt-installs nfs-common + in a privileged init container. Failures here block all NFS mounts — + but the pause container shows `1/1 Running` regardless, so we have + to tail the init container's logs separately.""" + _, out, _ = kctl_out( + "get", "pods", + "-l", "app.kubernetes.io/component=nfs-client-installer", + "--no-headers", + ) + if not out: + return + log.info(" nfs-installer: %s", out) + for line in out.splitlines(): + installer_pod = line.split()[0] + _, init_log, _ = kctl_out( + "logs", installer_pod, "-c", "install-nfs-common", + "--tail=20", timeout=5, + ) + for log_line in init_log.splitlines(): + key = ("installer", log_line) + if key not in self._seen: + self._seen.add(key) + log.info(" installer: %s", log_line) + + def _log_container_outputs(self): + for container in _LOG_CONTAINERS: + rc, out, _ = kctl_out( + "logs", self.pod, "-c", container, "--tail=20", timeout=5, + ) + if rc != 0: + continue + for line in out.splitlines(): + if line in self._seen_logs[container]: + continue + self._seen_logs[container].add(line) + log.info(" %s: %s", container, line) diff --git a/tests/e2e/_process.py b/tests/e2e/_process.py new file mode 100644 index 0000000..ad876f1 --- /dev/null +++ b/tests/e2e/_process.py @@ -0,0 +1,64 @@ +"""Subprocess execution helpers with structured logging. + +Two modes: + - run(...) : streams stdout live to logging (for slow commands + where you want to see progress). + - run(..., quiet=True): captures and logs after completion (for fast + queries where mid-stream output is just noise). + +`kctl()` is a thin wrapper that always injects `-n `. +""" + +import logging +import subprocess + +log = logging.getLogger("e2e") + +NAMESPACE = "default" + + +def run(*args, quiet=False, check=True, timeout=None): + """Run a subprocess. Returns CompletedProcess. + + quiet=False : live-stream stdout via log.info; stderr merged in. + quiet=True : capture, log all lines after completion (stdout=info, + stderr=warning). + """ + log.info("$ %s", " ".join(args)) + if quiet: + cp = subprocess.run(args, check=check, capture_output=True, + text=True, timeout=timeout) + for line in (cp.stdout or "").splitlines(): + log.info(" %s", line) + for line in (cp.stderr or "").splitlines(): + log.warning(" %s", line) + return cp + proc = subprocess.Popen(args, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, text=True, bufsize=1) + for line in proc.stdout: + log.info(" %s", line.rstrip()) + rc = proc.wait() + if check and rc != 0: + raise subprocess.CalledProcessError(rc, args) + return subprocess.CompletedProcess(args, rc) + + +def kctl(*args, quiet=True, **kw): + """kubectl wrapper that auto-namespaces. Defaults to quiet=True.""" + return run("kubectl", *args, "-n", NAMESPACE, quiet=quiet, **kw) + + +def kctl_out(*args, timeout=10): + """kubectl call returning (rc, stdout, stderr) without logging. + + Used by hot-loop pollers (PodObserver) where we'd otherwise spam the + log with one `$ kubectl ...` line every poll cycle. + """ + cp = subprocess.run(["kubectl", *args, "-n", NAMESPACE], + capture_output=True, text=True, timeout=timeout) + return cp.returncode, (cp.stdout or "").strip(), (cp.stderr or "").strip() + + +def step(n, total, msg): + """Log a progress banner: ──── [n/total] msg ────""" + log.info("──── [%d/%d] %s ────", n, total, msg) diff --git a/tests/e2e/conftest.py b/tests/e2e/conftest.py index 7d50155..6e963f1 100644 --- a/tests/e2e/conftest.py +++ b/tests/e2e/conftest.py @@ -1,97 +1,53 @@ -"""kind + helm fixtures for end-to-end tests. +"""End-to-end test fixtures. -Reuses an existing cluster if `KIND_CLUSTER` is set (fast iteration), -otherwise creates a throwaway one named `nbtest-e2e`. +Composes the helpers from `_cluster`, `_hub`, and `_pod_observer` into +pytest fixtures. The deep modules hide everything fiddly (cookie jars, +kubelet polling, force-cleanup); this file should stay short. + +Cluster reuse via env vars: + KIND_CLUSTER= use an existing cluster (skip create + delete) + KIND_KEEP=1 keep the cluster after a session that created it """ -import http.cookiejar -import json import logging import os import pathlib -import shutil import subprocess import time -import urllib.parse -import urllib.request import urllib.error +import urllib.request import pytest -log = logging.getLogger(__name__) +from _cluster import ( + ensure_cluster, + helm_install, + patch_nfs_hosts_entry, + require_binaries, + teardown_cluster, + wait_for_hub, +) +from _hub import HubClient +from _pod_observer import wait_for_pod_ready +from _process import NAMESPACE, kctl, kctl_out, step - -def _step(n, total, msg): - log.info("──── [%d/%d] %s ────", n, total, msg) +log = logging.getLogger("e2e") CLUSTER = os.environ.get("KIND_CLUSTER", "nbtest-e2e") RELEASE = "ds" -NAMESPACE = "default" HUB_LOCAL_PORT = 18000 TEST_VALUES = pathlib.Path(__file__).parent / "fixtures" / "test-values.yaml" -def _run(*args, check=True, capture=False): - """Run a subprocess. Streams stdout/stderr live via logging.""" - log.info("$ %s", " ".join(args)) - if capture: - cp = subprocess.run(args, check=check, capture_output=True, text=True) - for line in (cp.stdout or "").splitlines(): - log.info(" %s", line) - for line in (cp.stderr or "").splitlines(): - log.warning(" %s", line) - return cp - proc = subprocess.Popen(args, stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, text=True, bufsize=1) - for line in proc.stdout: - log.info(" %s", line.rstrip()) - rc = proc.wait() - if check and rc != 0: - raise subprocess.CalledProcessError(rc, args) - return subprocess.CompletedProcess(args, rc) - - -def _require(binary): - if not shutil.which(binary): - pytest.exit(f"{binary} not found on PATH", returncode=2) - - -def _cluster_exists(name): - cp = _run("kind", "get", "clusters", check=False, capture=True) - return name in (cp.stdout or "").splitlines() - - -def _wait_for_hub(timeout_s, poll_s): - """Poll until hub + proxy pods are Ready.""" - deadline = time.time() + timeout_s - attempt = 0 - while time.time() < deadline: - attempt += 1 - elapsed = int(timeout_s - (deadline - time.time())) - log.info("hub-wait attempt=%d elapsed=%ds", attempt, elapsed) - _run("kubectl", "get", "pods", "-n", NAMESPACE, - check=False, capture=True) - _run("kubectl", "logs", "-n", NAMESPACE, "-l", "component=hub", - "--tail=5", check=False, capture=True) - hub = _run("kubectl", "wait", "--for=condition=ready", "pod", - "-l", "component=hub", "-n", NAMESPACE, - "--timeout=2s", check=False, capture=True) - proxy = _run("kubectl", "wait", "--for=condition=ready", "pod", - "-l", "component=proxy", "-n", NAMESPACE, - "--timeout=2s", check=False, capture=True) - if hub.returncode == 0 and proxy.returncode == 0: - log.info("hub+proxy ready after %ds", elapsed) - return - time.sleep(poll_s) - log.error("timeout after %ds; dumping last 200 hub log lines", timeout_s) - _run("kubectl", "logs", "-n", NAMESPACE, "-l", "component=hub", - "--tail=200", check=False, capture=True) - pytest.fail(f"hub/proxy not ready within {timeout_s}s") +# --------------------------------------------------------------------------- +# Failure diagnostics +# --------------------------------------------------------------------------- @pytest.hookimpl(tryfirst=True, hookwrapper=True) def pytest_runtest_makereport(item, call): - """Stash test outcome on the item so fixtures can detect failures.""" + """Stash test outcome on the item so the diagnostics fixture can see + whether the test failed.""" outcome = yield rep = outcome.get_result() setattr(item, f"rep_{rep.when}", rep) @@ -99,7 +55,6 @@ def pytest_runtest_makereport(item, call): @pytest.fixture(autouse=True) def dump_diagnostics_on_failure(request): - """If the test fails, dump hub logs + pods + events for debugging.""" yield rep = getattr(request.node, "rep_call", None) if not (rep and rep.failed): @@ -107,143 +62,57 @@ def dump_diagnostics_on_failure(request): log.error("=" * 60) log.error("test failed: %s — dumping cluster diagnostics", request.node.name) log.error("=" * 60) - _run("kubectl", "get", "pods", "-n", NAMESPACE, - check=False, capture=True) - _run("kubectl", "get", "events", "-n", NAMESPACE, - "--sort-by=.lastTimestamp", check=False, capture=True) + kctl("get", "pods") + kctl("get", "events", "--sort-by=.lastTimestamp") log.error("--- hub logs (tail 200) ---") - _run("kubectl", "logs", "-n", NAMESPACE, "-l", "component=hub", - "--tail=200", check=False, capture=True) - log.error("--- singleuser pod logs (any user) ---") - _run("kubectl", "logs", "-n", NAMESPACE, - "-l", "component=singleuser-server", "--tail=100", - "--all-containers=true", "--prefix=true", - check=False, capture=True) - - -def _patch_nfs_pv_to_cluster_ip(): - """Add the NFS service FQDN to the kind node's /etc/hosts. - - kind nodes' host /etc/resolv.conf only knows Docker DNS, so `mount.nfs` - (which runs in the host mount namespace) can't resolve cluster-internal - FQDNs. Patching the PV's `nfs.server` to the IP would work but is - rejected as immutable once the PV is Bound. Adding a hosts entry on - each kind node bypasses DNS entirely and works post-bind. - Production clusters don't need this — their kubelets have cluster DNS. - """ - rc, pv_name, _ = _kctl( - "get", "pv", "-l", - f"app.kubernetes.io/instance={RELEASE}", - "-o", "jsonpath={.items[?(@.spec.nfs)].metadata.name}", - ) - pv_name = pv_name.strip() - if not pv_name: - log.info("no NFS-backed PV found — skipping hosts entry") - return + kctl("logs", "-l", "component=hub", "--tail=200", check=False) + log.error("--- singleuser pod logs ---") + kctl("logs", "-l", "component=singleuser-server", "--tail=100", + "--all-containers=true", "--prefix=true", check=False) - rc, fqdn, _ = _kctl( - "get", "pv", pv_name, - "-o", "jsonpath={.spec.nfs.server}", - ) - fqdn = fqdn.strip() - svc_name = fqdn.split(".", 1)[0] - rc, svc_ip, err = _kctl( - "get", "svc", svc_name, - "-o", "jsonpath={.spec.clusterIP}", - ) - svc_ip = svc_ip.strip() - if not svc_ip: - log.error("could not resolve NFS svc %s ClusterIP: %s", svc_name, err) - return - # Add to each kind node's /etc/hosts (idempotent: skip if already there). - cp = subprocess.run( - ["kind", "get", "nodes", "--name", CLUSTER], - capture_output=True, text=True, check=True, - ) - for node in cp.stdout.strip().splitlines(): - cmd = (f"grep -q '{fqdn}$' /etc/hosts || " - f"echo '{svc_ip} {fqdn}' >> /etc/hosts") - log.info("hosts entry on %s: %s -> %s", node, fqdn, svc_ip) - subprocess.run(["docker", "exec", node, "sh", "-c", cmd], check=True) +# --------------------------------------------------------------------------- +# Cluster + chart (session-scoped) +# --------------------------------------------------------------------------- @pytest.fixture(scope="session") def cluster(): - TOTAL = 7 - _step(1, TOTAL, "verify required binaries on PATH") - for b in ("kind", "helm", "kubectl"): - _require(b) - - created_here = False - if not _cluster_exists(CLUSTER): - _step(2, TOTAL, f"create kind cluster '{CLUSTER}'") - _run("kind", "create", "cluster", "--name", CLUSTER, "--wait", "60s") - created_here = True - else: - _step(2, TOTAL, f"reuse existing kind cluster '{CLUSTER}'") - # `kind delete` can wipe the kubeconfig entry while leaving the - # container; re-export to make sure the context is wired up. - _run("kind", "export", "kubeconfig", "--name", CLUSTER, - check=True, capture=True) - - _step(3, TOTAL, "helm dependency update") - _run("helm", "dependency", "update") - - _step(4, TOTAL, "helm install (chart + test overrides)") - _t = time.time() - _run("helm", "upgrade", "--install", RELEASE, ".", - "--namespace", NAMESPACE, - "--set", "nebariapp.enabled=false", - "--values", str(TEST_VALUES)) - log.info("helm install completed in %ds", int(time.time() - _t)) - - _step(5, TOTAL, "patch NFS PV server field to ClusterIP (kind workaround)") - _patch_nfs_pv_to_cluster_ip() - - _step(6, TOTAL, "snapshot post-install resources") - _run("kubectl", "get", "all,pvc,configmap", "-n", NAMESPACE, - check=False, capture=True) - - _step(7, TOTAL, "wait for hub + proxy ready") - _wait_for_hub(timeout_s=300, poll_s=5) + TOTAL = 6 + step(1, TOTAL, "verify required binaries on PATH") + require_binaries("kind", "helm", "kubectl") + + step(2, TOTAL, f"ensure kind cluster '{CLUSTER}'") + created_here = ensure_cluster(CLUSTER) + log.info("created_here=%s", created_here) + + step(3, TOTAL, "helm install (chart + test overrides)") + helm_install(RELEASE, ".", TEST_VALUES) + + step(4, TOTAL, "kind workaround: NFS svc FQDN -> /etc/hosts") + patch_nfs_hosts_entry(RELEASE, CLUSTER) + + step(5, TOTAL, "snapshot post-install resources") + kctl("get", "all,pvc,configmap") + + step(6, TOTAL, "wait for hub + proxy ready") + wait_for_hub() yield CLUSTER if created_here and not os.environ.get("KIND_KEEP"): log.info("deleting cluster %s", CLUSTER) - cp = _run("kind", "delete", "cluster", "--name", CLUSTER, check=False, - capture=True) - if cp.returncode != 0: - # Docker Desktop occasionally fails to kill a container with - # active mounts. Force-remove orphan node containers so the next - # run starts clean. - log.warning("kind delete failed; force-removing node containers") - cp2 = subprocess.run( - ["docker", "ps", "-a", "--filter", - f"name={CLUSTER}-", "--format", "{{.Names}}"], - capture_output=True, text=True, - ) - for name in (cp2.stdout or "").strip().splitlines(): - # `docker stop` first with timeout=0 (SIGKILL immediately), - # then rm -f. Avoids the "did not receive an exit event" hang. - subprocess.run( - ["docker", "stop", "--time=0", name], - capture_output=True, - ) - subprocess.run( - ["docker", "rm", "-f", "-v", name], - capture_output=True, - ) - subprocess.run( - ["docker", "network", "rm", f"kind"], - capture_output=True, - ) + teardown_cluster(CLUSTER) + + +# --------------------------------------------------------------------------- +# Per-test: port-forward + hub session +# --------------------------------------------------------------------------- @pytest.fixture def hub_url(cluster): - """Port-forward proxy-public, yield base URL, tear down.""" + """Port-forward proxy-public; yield base URL; tear down.""" log.info("port-forward svc/proxy-public -> :%d", HUB_LOCAL_PORT) pf = subprocess.Popen( ["kubectl", "port-forward", "svc/proxy-public", @@ -252,16 +121,7 @@ def hub_url(cluster): ) base = f"http://localhost:{HUB_LOCAL_PORT}" try: - deadline = time.time() + 30 - while time.time() < deadline: - try: - with urllib.request.urlopen(f"{base}/hub/login", timeout=2) as r: - if r.status == 200: - break - except (urllib.error.URLError, ConnectionResetError): - time.sleep(0.5) - else: - pytest.fail("port-forward never became reachable") + _wait_for_url(f"{base}/hub/login", timeout_s=30) log.info("port-forward ready at %s", base) yield base finally: @@ -270,308 +130,89 @@ def hub_url(cluster): pf.wait(timeout=5) +def _wait_for_url(url, timeout_s): + deadline = time.time() + timeout_s + while time.time() < deadline: + try: + with urllib.request.urlopen(url, timeout=2) as r: + if r.status == 200: + return + except (urllib.error.URLError, ConnectionResetError): + time.sleep(0.5) + pytest.fail(f"{url} never became reachable") + + +# --------------------------------------------------------------------------- +# Per-test: spawn a singleuser pod +# --------------------------------------------------------------------------- + + class SpawnedUser: """Handle to a logged-in JupyterHub user with a running singleuser pod.""" def __init__(self, login_name, real_user, pod): - self.login_name = login_name # e.g. "alice-data" (sent to /hub/login) - self.user = real_user # e.g. "alice" (authenticator-resolved) + self.login_name = login_name # e.g. "alice-data" (form input) + self.user = real_user # e.g. "alice" (auth-resolved) self.pod = pod # k8s pod name def exec(self, *cmd, user=None): - """Run a command inside the singleuser pod, return (rc, stdout).""" + """Run a command inside the notebook container. Returns (rc, out).""" flags = ["-n", NAMESPACE, self.pod, "-c", "notebook", "--"] if user: - return _kexec(*flags, "su", "-", user, "-c", " ".join(cmd)) - return _kexec(*flags, *cmd) + return _kubectl_exec(*flags, "su", "-", user, "-c", " ".join(cmd)) + return _kubectl_exec(*flags, *cmd) -def _kexec(*args): - cp = subprocess.run( - ["kubectl", "exec", *args], - capture_output=True, text=True, - ) - # Concatenate stderr after stdout for diagnostics on failure but the - # primary signal (return code, stdout) is what the test asserts on. - out = (cp.stdout or "") + (cp.stderr or "") - return cp.returncode, out - - -def _login_and_spawn(base, login_name, timeout_s=180): - """POST /hub/login + start server + wait for pod ready. Returns SpawnedUser.""" - SPAWN_TOTAL = 4 - jar = http.cookiejar.CookieJar() - opener = urllib.request.build_opener( - urllib.request.HTTPCookieProcessor(jar), - ) - - def _xsrf(): - for c in jar: - if c.name == "_xsrf": - return c.value - return "" - - def _request(method, path, data=None, headers=None): - url = base + path - body = urllib.parse.urlencode(data).encode() if data else None - req = urllib.request.Request(url, data=body, method=method, - headers=headers or {}) - try: - r = opener.open(req, timeout=15) - payload = r.read().decode(errors="replace") - return r.status, dict(r.headers), payload - except urllib.error.HTTPError as e: - payload = e.read().decode(errors="replace") if e.fp else "" - return e.code, dict(e.headers or {}), payload - - # 1. Prime _xsrf cookie - _step(1, SPAWN_TOTAL, "GET /hub/login (prime _xsrf cookie)") - status, _, _ = _request("GET", "/hub/login") - log.info(" status=%d cookies=%s", status, [c.name for c in jar]) - - # 2. Login (DummyAuthenticator accepts any password). Retry on 5xx — the - # hub briefly returns 503 right after a previous user pod is deleted - # while it cleans up the spawner state. - _step(2, SPAWN_TOTAL, f"POST /hub/login (as {login_name})") - deadline = time.time() + 30 - while True: - status, _, body = _request( - "POST", "/hub/login", - data={"username": login_name, "password": "x", "_xsrf": _xsrf()}, - ) - log.info(" status=%d body[:200]=%r", status, body[:200]) - if status in (200, 302): - break - if status >= 500 and time.time() < deadline: - log.info(" retrying after 5xx in 2s") - time.sleep(2) - continue - pytest.fail(f"login failed status={status}: {body}") - - # 3. Start the server - real_user = login_name.split("-")[0] - _step(3, SPAWN_TOTAL, f"POST /hub/api/users/{real_user}/server") - status, headers, body = _request( - "POST", f"/hub/api/users/{real_user}/server", - headers={"X-XSRFToken": _xsrf()}, - ) - log.info(" status=%d body=%s", status, body) - if status not in (201, 202, 400): # 400 = already running - log.error("spawn POST returned %d", status) - log.error(" response headers: %s", headers) - log.error(" response body: %s", body) - log.error(" request cookies: %s", [c.name for c in jar]) - pytest.fail(f"spawn POST returned {status}: {body}") - - # 4. Wait for pod ready - pod_label = f"hub.jupyter.org/username={real_user}" - _step(4, SPAWN_TOTAL, f"wait for pod with label {pod_label}") - deadline = time.time() + timeout_s - pod_name = None - while time.time() < deadline: - cp = _run("kubectl", "get", "pods", "-n", NAMESPACE, - "-l", pod_label, "-o", "jsonpath={.items[0].metadata.name}", - check=False, capture=True) - if cp.returncode == 0 and cp.stdout.strip(): - pod_name = cp.stdout.strip() - break - time.sleep(2) - if not pod_name: - pytest.fail(f"pod for user {real_user} never appeared") - - _wait_for_pod_ready(pod_name, timeout_s=timeout_s, poll_s=5) - return SpawnedUser(login_name, real_user, pod_name) - - -_INIT_CONTAINERS = ("block-cloud-metadata", "initialize-shared-mounts") - - -def _kctl(*args, timeout=10): - """Quiet kubectl helper that returns trimmed stdout (no live logging).""" - cp = subprocess.run(["kubectl", *args, "-n", NAMESPACE], - capture_output=True, text=True, timeout=timeout) - return cp.returncode, (cp.stdout or "").strip(), (cp.stderr or "").strip() - - -def _wait_for_pod_ready(pod_name, timeout_s, poll_s): - """Poll pod state until Ready, surfacing every observable kubelet signal. - - Each cycle logs: - - one-line pod summary (phase, container/init readiness) - - new events since the last poll (image pull, FailedMount, scheduling…) - - nfs-client-installer DaemonSet phase (PR #30 prereq for NFS) - - tail of any init/notebook container logs that have started - """ - deadline = time.time() + timeout_s - attempt = 0 - seen_events: set[str] = set() - seen_log_lines: dict[str, set[str]] = {c: set() for c in _INIT_CONTAINERS} - while time.time() < deadline: - attempt += 1 - elapsed = int(timeout_s - (deadline - time.time())) - log.info("=== pod-wait %s attempt=%d elapsed=%ds ===", - pod_name, attempt, elapsed) - - # 1. Pod summary - rc, out, _ = _kctl("get", "pod", pod_name, "--no-headers") - if rc == 0: - log.info(" pod: %s", out) - else: - log.warning(" pod not found yet") - - # 2. Per-container state (more readable than jsonpath blob) - rc, out, _ = _kctl( - "get", "pod", pod_name, "-o", - "jsonpath={range .status.initContainerStatuses[*]}" - "init/{.name} ready={.ready} state={.state}{'\\n'}{end}" - "{range .status.containerStatuses[*]}" - "main/{.name} ready={.ready} state={.state}{'\\n'}{end}", - ) - for line in out.splitlines(): - log.info(" %s", line) - - # 3. Pod events from kubectl describe (canonical, with aggregation). - # Dedup on (Type, Reason, Message) — drop the Age column which - # keeps changing and breaks naive string-equality dedup. - rc, out, _ = _kctl("describe", "pod", pod_name, timeout=10) - in_events = False - for raw in out.splitlines(): - stripped = raw.strip() - if raw.startswith("Events:"): - in_events = True; continue - if raw and not raw.startswith(" "): - in_events = False - if not (in_events and stripped): - continue - if stripped.startswith("Type") or stripped.startswith("----"): - continue - # describe-pod event row: "Type Reason Age From Message" - # Drop column index 2 (Age) for stable dedup. - cols = stripped.split(None, 4) - key = (cols[0], cols[1], cols[3] if len(cols) > 3 else "", - cols[4] if len(cols) > 4 else "") - if key in seen_events: - continue - seen_events.add(key) - log.info(" event: %s", stripped) - - # 4. Node-level kubelet logs (the deepest signal: NFS mount attempts, - # image pull progress, container exec). kind nodes are docker - # containers; journalctl is available inside. - rc, out, _ = _kctl( - "get", "pod", pod_name, "-o", "jsonpath={.spec.nodeName}", - ) - if out: - node = out.strip() - cp = subprocess.run( - ["docker", "exec", node, "journalctl", "-u", "kubelet", - "--since", "8 seconds ago", "--no-pager", "-q", - "-o", "cat"], - capture_output=True, text=True, timeout=5, - ) - for line in (cp.stdout or "").splitlines(): - # Filter to lines that mention this pod's name or its uid. - if pod_name in line or "FailedMount" in line or "MountVolume" in line: - if line in seen_events: - continue - seen_events.add(line) - log.info(" kubelet: %s", line) - - # 4. NFS client installer DaemonSet status + init-container logs. - # The "1/1 Running" we see is the pause container — the actual - # apt-install runs in an init container; failures hide unless - # we tail its logs. - rc, out, _ = _kctl( - "get", "pods", "-l", - "app.kubernetes.io/component=nfs-client-installer", - "--no-headers", - ) - if out: - log.info(" nfs-installer: %s", out) - for installer_pod in (line.split()[0] for line in out.splitlines()): - rc2, init_log, _ = _kctl( - "logs", installer_pod, "-c", "install-nfs-common", - "--tail=20", timeout=5, - ) - if rc2 == 0: - for line in (init_log or "").splitlines(): - key = ("installer-log", line) - if key in seen_events: - continue - seen_events.add(key) - log.info(" installer: %s", line) - - # 5. Init + main container logs as they start producing output. - for c in _INIT_CONTAINERS + ("notebook",): - rc, out, _ = _kctl( - "logs", pod_name, "-c", c, "--tail=20", timeout=5, - ) - if rc != 0: - continue - for line in out.splitlines(): - if line in seen_log_lines.setdefault(c, set()): - continue - seen_log_lines[c].add(line) - log.info(" %s: %s", c, line) - - # Ready? - ready = subprocess.run( - ["kubectl", "wait", "--for=condition=ready", "pod", pod_name, - "-n", NAMESPACE, "--timeout=2s"], - capture_output=True, text=True, - ) - if ready.returncode == 0: - log.info("pod %s ready after %ds", pod_name, elapsed) - return - time.sleep(poll_s) - pytest.fail(f"pod {pod_name} not ready within {timeout_s}s") +def _kubectl_exec(*args): + cp = subprocess.run(["kubectl", "exec", *args], + capture_output=True, text=True) + return cp.returncode, (cp.stdout or "") + (cp.stderr or "") @pytest.fixture def spawn_user(hub_url): - """Factory: login and start a singleuser pod for a username convention. + """Factory: log in + start a singleuser pod for a username convention. - Username 'alice-data-ml' -> User(name='alice', groups=['data','ml']). - Pods are stopped and deleted in teardown. + Username 'alice-data-ml' → User(name='alice', groups=['data','ml']). + Pods are stopped via the JupyterHub API in teardown. """ + client = HubClient(hub_url) spawned: list[SpawnedUser] = [] def _spawn(login_name): - u = _login_and_spawn(hub_url, login_name) + SPAWN_STEPS = 3 + step(1, SPAWN_STEPS, f"login as {login_name}") + client.login(login_name) + + real_user = login_name.split("-")[0] + step(2, SPAWN_STEPS, f"spawn server for {real_user}") + client.spawn(real_user) + + step(3, SPAWN_STEPS, f"wait for pod ready (user={real_user})") + pod = _wait_for_pod_to_appear(real_user) + wait_for_pod_ready(pod) + + u = SpawnedUser(login_name, real_user, pod) spawned.append(u) return u yield _spawn - # Stop via the JupyterHub API so the spawner state is cleaned up (a - # raw pod delete leaves the hub thinking the server is still pending, - # causing 503s on the next login). for u in spawned: log.info("stopping server for %s via /hub/api", u.user) - _stop_server(hub_url, u.login_name, u.user) + client.stop(u.user) -def _stop_server(base, login_name, real_user): - """DELETE /hub/api/users//server (login first to get auth cookie).""" - jar = http.cookiejar.CookieJar() - opener = urllib.request.build_opener( - urllib.request.HTTPCookieProcessor(jar)) - try: - opener.open(base + "/hub/login", timeout=10).read() - xsrf = next((c.value for c in jar if c.name == "_xsrf"), "") - opener.open(urllib.request.Request( - base + "/hub/login", method="POST", - data=urllib.parse.urlencode( - {"username": login_name, "password": "x", "_xsrf": xsrf} - ).encode(), - ), timeout=10).read() - xsrf = next((c.value for c in jar if c.name == "_xsrf"), "") - opener.open(urllib.request.Request( - base + f"/hub/api/users/{real_user}/server", - method="DELETE", - headers={"X-XSRFToken": xsrf}, - ), timeout=15).read() - except urllib.error.HTTPError as e: - log.warning("stop_server: %s -> %d", real_user, e.code) - except Exception as e: - log.warning("stop_server failed: %s", e) +def _wait_for_pod_to_appear(real_user, timeout_s=60): + """Wait for the singleuser pod object to be created (named by hub).""" + label = f"hub.jupyter.org/username={real_user}" + deadline = time.time() + timeout_s + while time.time() < deadline: + rc, name, _ = kctl_out( + "get", "pods", "-l", label, + "-o", "jsonpath={.items[0].metadata.name}", + ) + if rc == 0 and name: + return name + time.sleep(2) + pytest.fail(f"pod for user {real_user} never appeared") From ec48c194368408e091694c0959ac80dd474539a9 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Mon, 27 Apr 2026 14:43:50 +0100 Subject: [PATCH 18/60] =?UTF-8?q?ci:=20speed=20up=20e2e=20=E2=80=94=20disa?= =?UTF-8?q?ble=20z2jh=20prePuller=20+=20cache=20kindest/node?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit prePuller hooks pre-pull singleuser images on every node before helm install completes. On a single-node test cluster this is pure overhead (~30s of blocking wait). Disable in test-values.yaml. kindest/node image pull was the largest variable cost in CI: 9s on a fast runner, 130s on a slow one. Cache it as a docker tarball keyed on the kind version so subsequent runs are deterministic and fast. Expected: total CI time drops from variable 3-5min to ~90-120s. --- .github/workflows/test.yaml | 28 ++++++++++++++++++++++++++++ tests/e2e/fixtures/test-values.yaml | 9 +++++++++ 2 files changed, 37 insertions(+) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 67428c5..bfc404b 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -26,12 +26,40 @@ jobs: - name: Install uv run: curl -LsSf https://astral.sh/uv/install.sh | sh + # Cache the kindest/node image as a docker tarball. Pulling it from + # the registry was the largest variable cost in CI (saw 9s → 130s + # depending on runner network), so caching makes runtimes + # deterministic. Cache key includes the kind version (which fixes + # the node-image tag). + - name: Cache kindest/node image + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: /tmp/kind-images/kindest-node.tar + key: kindest-node-kind-v0.27.0 + + - name: Load cached kindest/node (if present) + run: | + mkdir -p /tmp/kind-images + if [ -f /tmp/kind-images/kindest-node.tar ]; then + docker load -i /tmp/kind-images/kindest-node.tar + fi + - name: Run e2e tests env: KIND_KEEP: "1" # runner is ephemeral, skip teardown PYTHONUNBUFFERED: "1" # stream logs live (no block buffering on non-TTY) run: uvx pytest tests/e2e -v --color=yes + # Save kindest/node tarball after first run so subsequent runs hit + # the cache. Skipped on cache hit to avoid useless rewrites. + - name: Save kindest/node for cache + if: success() + run: | + if [ ! -f /tmp/kind-images/kindest-node.tar ]; then + img=$(docker images --format '{{.Repository}}:{{.Tag}}' | grep '^kindest/node:' | head -1) + [ -n "$img" ] && docker save "$img" -o /tmp/kind-images/kindest-node.tar + fi + - name: Hub logs on failure if: failure() run: kubectl logs -l component=hub --tail=200 || true diff --git a/tests/e2e/fixtures/test-values.yaml b/tests/e2e/fixtures/test-values.yaml index b4388d9..6fa0645 100644 --- a/tests/e2e/fixtures/test-values.yaml +++ b/tests/e2e/fixtures/test-values.yaml @@ -19,6 +19,15 @@ sharedStorage: - nfsvers=3 jupyterhub: + # z2jh's prePuller hooks pre-pull singleuser images on every node before + # helm install completes. On a single-node test cluster this is pure + # overhead (~30s of helm-install wait) — disable. + prePuller: + hook: + enabled: false + continuous: + enabled: false + custom: shared-storage-enabled: true shared-storage-mount-prefix: "/shared" From e8362063902a9dde1721a1bcb83ff9587c21c8ef Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Mon, 27 Apr 2026 14:48:12 +0100 Subject: [PATCH 19/60] ci: fix kindest/node cache-save step under set -e Previous heuristic used `[ -n "$img" ] && docker save` which exits 1 when grep finds no image, killing the whole step. Hardcode the v1.32.2 tag (fixed by kind v0.27.0) and use plain commands so set -e only triggers on real failures. --- .github/workflows/test.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index bfc404b..8c4c7ed 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -51,14 +51,14 @@ jobs: run: uvx pytest tests/e2e -v --color=yes # Save kindest/node tarball after first run so subsequent runs hit - # the cache. Skipped on cache hit to avoid useless rewrites. + # the cache. Skipped on cache hit. Image tag is fixed by kind v0.27.0. - name: Save kindest/node for cache if: success() run: | - if [ ! -f /tmp/kind-images/kindest-node.tar ]; then - img=$(docker images --format '{{.Repository}}:{{.Tag}}' | grep '^kindest/node:' | head -1) - [ -n "$img" ] && docker save "$img" -o /tmp/kind-images/kindest-node.tar - fi + set -e + [ -f /tmp/kind-images/kindest-node.tar ] && exit 0 + docker images + docker save kindest/node:v1.32.2 -o /tmp/kind-images/kindest-node.tar - name: Hub logs on failure if: failure() From 9de8ef388880e0609e144e2ed14f1ac86cf5a45a Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Mon, 27 Apr 2026 14:52:20 +0100 Subject: [PATCH 20/60] ci: drop kindest/node cache attempt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GH Actions ubuntu-latest runners come with kindest/node preinstalled (tagged ""). The actual image fetch on cache miss was only ~12s because docker just verifies the digest. The cache step was earning ~5s in the best case and breaking the workflow when docker save couldn't find the v1.32.2 tag (image is referenced by digest, not tag). prePuller-disable change is keeping its ~30s saving — sufficient win without the cache complexity. --- .github/workflows/test.yaml | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 8c4c7ed..67428c5 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -26,40 +26,12 @@ jobs: - name: Install uv run: curl -LsSf https://astral.sh/uv/install.sh | sh - # Cache the kindest/node image as a docker tarball. Pulling it from - # the registry was the largest variable cost in CI (saw 9s → 130s - # depending on runner network), so caching makes runtimes - # deterministic. Cache key includes the kind version (which fixes - # the node-image tag). - - name: Cache kindest/node image - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 - with: - path: /tmp/kind-images/kindest-node.tar - key: kindest-node-kind-v0.27.0 - - - name: Load cached kindest/node (if present) - run: | - mkdir -p /tmp/kind-images - if [ -f /tmp/kind-images/kindest-node.tar ]; then - docker load -i /tmp/kind-images/kindest-node.tar - fi - - name: Run e2e tests env: KIND_KEEP: "1" # runner is ephemeral, skip teardown PYTHONUNBUFFERED: "1" # stream logs live (no block buffering on non-TTY) run: uvx pytest tests/e2e -v --color=yes - # Save kindest/node tarball after first run so subsequent runs hit - # the cache. Skipped on cache hit. Image tag is fixed by kind v0.27.0. - - name: Save kindest/node for cache - if: success() - run: | - set -e - [ -f /tmp/kind-images/kindest-node.tar ] && exit 0 - docker images - docker save kindest/node:v1.32.2 -o /tmp/kind-images/kindest-node.tar - - name: Hub logs on failure if: failure() run: kubectl logs -l component=hub --tail=200 || true From 1c89ab2c619932e8596e89b570cfc77b8d45376b Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Thu, 30 Apr 2026 14:15:34 +0100 Subject: [PATCH 21/60] test(e2e): expand shared-storage suite to full permission contract MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 9 tests (was 2) covering the per-group /shared/ contract end-to-end: - dir is root:users 2775 (parametrized over groups) - pod is member of users group; NB_UMASK=0002 in env - new files inherit gid=100 mode 0664; new subdirs gid=100 mode 02775 (setgid propagation) - multi-group user sees + writes every group dir - user does not see groups they don't belong to (mount-time isolation) - file written by one user is readable + appendable by a groupmate from a separate pod (cross-user collaboration) Conftest adds PathStat + SpawnedUser.stat()/path_exists() so tests assert against typed fields (mode/uid/gid) instead of parsing stat strings — keeps tests short and behavior-focused. --- tests/e2e/conftest.py | 35 ++++++- tests/e2e/test_shared_storage.py | 151 +++++++++++++++++++++++++++++-- 2 files changed, 176 insertions(+), 10 deletions(-) diff --git a/tests/e2e/conftest.py b/tests/e2e/conftest.py index 6e963f1..659d5c6 100644 --- a/tests/e2e/conftest.py +++ b/tests/e2e/conftest.py @@ -147,6 +147,26 @@ def _wait_for_url(url, timeout_s): # --------------------------------------------------------------------------- +class PathStat: + """Decoded `stat` output for one path inside a singleuser pod. + + Tests assert against typed fields rather than parsing `stat` strings: + + s = u.stat("/shared/data") + assert s.mode == 0o2775 and s.gid == USERS_GID + """ + + __slots__ = ("mode", "uid", "gid") + + def __init__(self, mode, uid, gid): + self.mode = mode # int, e.g. 0o2775 + self.uid = uid # int + self.gid = gid # int + + def __repr__(self): + return f"PathStat(mode=0o{self.mode:o}, uid={self.uid}, gid={self.gid})" + + class SpawnedUser: """Handle to a logged-in JupyterHub user with a running singleuser pod.""" @@ -162,11 +182,24 @@ def exec(self, *cmd, user=None): return _kubectl_exec(*flags, "su", "-", user, "-c", " ".join(cmd)) return _kubectl_exec(*flags, *cmd) + def stat(self, path): + """Return PathStat for `path` inside the pod. Fails the test if the + path is missing — use `path_exists()` first if absence is expected.""" + rc, out = self.exec("stat", "-c", "%a %u %g", path) + if rc != 0: + pytest.fail(f"stat {path} on pod {self.pod} failed: {out}") + mode_s, uid_s, gid_s = out.split() + return PathStat(int(mode_s, 8), int(uid_s), int(gid_s)) + + def path_exists(self, path): + rc, _ = self.exec("test", "-e", path) + return rc == 0 + def _kubectl_exec(*args): cp = subprocess.run(["kubectl", "exec", *args], capture_output=True, text=True) - return cp.returncode, (cp.stdout or "") + (cp.stderr or "") + return cp.returncode, ((cp.stdout or "") + (cp.stderr or "")).strip() @pytest.fixture diff --git a/tests/e2e/test_shared_storage.py b/tests/e2e/test_shared_storage.py index 36acf89..6b311fa 100644 --- a/tests/e2e/test_shared_storage.py +++ b/tests/e2e/test_shared_storage.py @@ -1,16 +1,149 @@ -"""Behavior tests for PR #30 shared-storage feature.""" +"""End-to-end behavior of /shared/ directories. +Per-group RWX shared dirs at /shared/ are set up by the chart with: -def test_user_in_group_can_write(spawn_user): - """alice (in group 'data') can write to /shared/data/.""" + - ownership root:users (uid=0, gid=100), mode 2775 (setgid on 'users') + - pod runs with fsGroup=100 + NB_UMASK=0002 so new files are 664/775, + group-writable, and inherit gid=100 by setgid propagation + - only the groups a user belongs to are mounted (no cross-group leakage) + - shared dir is RWX across all members of a group (multi-pod, multi-user) + +Test usernames encode group membership via the test DummyAuthenticator +(see tests/e2e/fixtures/test-values.yaml): + + "alice-data" -> User('alice', groups=['data']) + "alice-data-ml" -> User('alice', groups=['data','ml']) + "bob-ml" -> User('bob', groups=['ml']) +""" + +import pytest + +# Constants used in assertions across the suite. They read out as English +# next to `==` so a failing assertion explains itself. +USERS_GID = 100 # Linux 'users' group; nebari's fsGroup +ROOT_UID = 0 # init container chown's dir to root for setgid +SHARED_DIR_MODE = 0o2775 # rwxrwsr-x — setgid + group-writable +EXPECTED_FILE_MODE = 0o664 # under umask 0002 +EXPECTED_DIR_MODE = 0o2775 # umask 0002 + setgid propagated from parent + + +# --- Helpers --------------------------------------------------------------- + + +def _write_under_pod_umask(user, shell_cmd): + """Run `shell_cmd` with umask taken from NB_UMASK. + + `kubectl exec` starts a fresh shell that does NOT inherit the umask + z2jh's start.sh applied to the kernel process — so tests have to + re-apply it explicitly to observe the documented behavior. + """ + rc, out = user.exec("bash", "-c", f'umask "$NB_UMASK"; {shell_cmd}') + assert rc == 0, f"setup command failed (rc={rc}): {out}" + + +# --- Directory attributes (chart-rendered, before any user write) ---------- + + +@pytest.mark.parametrize("group", ["data", "ml"]) +def test_group_dir_is_root_users_with_setgid_2775(spawn_user, group): + """Per-group dir is owned root:users, mode 2775. Setgid forces gid=100 + on every new file regardless of the creator's primary gid — this is + what makes shared collaboration work across users.""" + u = spawn_user(f"alice-{group}") + s = u.stat(f"/shared/{group}") + assert s.uid == ROOT_UID + assert s.gid == USERS_GID + assert s.mode == SHARED_DIR_MODE + + +# --- Pod identity (groups + umask the chart configured) -------------------- + + +def test_pod_is_member_of_users_group(spawn_user): + """fsGroup=100 — pod's effective gids include 100, which is what + grants it write access to the group-writable shared dirs.""" + u = spawn_user("alice-data") + rc, out = u.exec("id", "-G") + assert rc == 0 + assert str(USERS_GID) in out.split() + + +def test_pod_environment_sets_nb_umask_to_0002(spawn_user): + """NB_UMASK=0002 is the env var z2jh's start.sh applies before exec'ing + the kernel. The umask effect is covered by the file-mode test below; + here we just pin the configuration contract.""" + u = spawn_user("alice-data") + rc, out = u.exec("printenv", "NB_UMASK") + assert rc == 0 + assert out == "0002" + + +# --- File/dir creation inherits group + umask ------------------------------ + + +def test_new_file_is_group_writable_and_owned_by_users(spawn_user): + """A file created in /shared/ ends up mode 664, gid 100. This + is the core multi-tenancy invariant: any teammate can edit any other + teammate's files without explicit coordination.""" u = spawn_user("alice-data") - rc, out = u.exec("touch", "/shared/data/file_from_alice") - assert rc == 0, f"expected write to succeed, got rc={rc}: {out}" + _write_under_pod_umask(u, "touch /shared/data/file_from_alice") + + s = u.stat("/shared/data/file_from_alice") + assert s.gid == USERS_GID + assert s.mode == EXPECTED_FILE_MODE -def test_shared_dir_is_group_owned(spawn_user): - """/shared/data has gid matching the 'data' group and mode 2775 (setgid).""" +def test_new_subdir_inherits_setgid_and_users_group(spawn_user): + """A subdir created under a setgid parent inherits the setgid bit and + gid=100. Without this, nested files would silently fall back to the + user's primary gid and become invisible to teammates.""" u = spawn_user("alice-data") - rc, out = u.exec("stat", "-c", "%a", "/shared/data") + _write_under_pod_umask(u, "mkdir /shared/data/subdir_from_alice") + + s = u.stat("/shared/data/subdir_from_alice") + assert s.gid == USERS_GID + assert s.mode == EXPECTED_DIR_MODE + + +# --- Multi-tenancy across users and groups --------------------------------- + + +def test_user_in_multiple_groups_sees_each_groups_dir(spawn_user): + """A user who belongs to N groups gets N per-group dirs mounted, each + of them writable. Group membership composes — there is no max.""" + u = spawn_user("alice-data-ml") + + for group in ("data", "ml"): + path = f"/shared/{group}" + assert u.path_exists(path), f"{path} should be mounted for alice" + rc, out = u.exec("touch", f"{path}/probe-{group}") + assert rc == 0, f"write to {path} failed: {out}" + + +def test_user_does_not_see_groups_they_dont_belong_to(spawn_user): + """Group isolation is enforced at mount time: a user not in group X + does not get /shared/X mounted at all (as opposed to mounted-but- + unreadable). Cleaner failure mode and one less attack surface.""" + u = spawn_user("bob-ml") + assert u.path_exists("/shared/ml") + assert not u.path_exists("/shared/data") + + +def test_files_are_visible_and_writable_to_groupmates(spawn_user): + """alice and carol both belong to 'data'. alice writes a file from + her pod; carol reads + appends to it from hers. Same RWX PVC, same + subPath, same setgid'd gid=100 — the actual collaboration story.""" + alice = spawn_user("alice-data") + _write_under_pod_umask( + alice, "echo hello-from-alice > /shared/data/handoff.txt" + ) + + carol = spawn_user("carol-data") + rc, out = carol.exec("cat", "/shared/data/handoff.txt") assert rc == 0 - assert out.strip() == "2775", f"expected mode 2775, got {out.strip()!r}" + assert out == "hello-from-alice" + + rc, out = carol.exec( + "bash", "-c", "echo carol-was-here >> /shared/data/handoff.txt" + ) + assert rc == 0, f"carol could not append to alice's file: {out}" From e39b3b2dedcc1bf9f5696a02793d656c301a51ef Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Thu, 30 Apr 2026 14:55:04 +0100 Subject: [PATCH 22/60] ci: cache singleuser image across runs to skip ~73s cold pull MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Singleuser image (multi-GB) is currently pulled by kubelet inside the kind node on first user spawn, costing ~73s of every CI run. Pull it once on the runner host, save as tar, cache it (key = image ref so a values.yaml bump auto-invalidates), and side-load with `kind load image-archive`. Pre-create the cluster in the workflow so the side-load happens before any pod is scheduled — the pytest fixture's ensure_cluster() reuses the existing cluster. Cache hit: skips the ~90s registry pull entirely; only kind-load (~20s) remains. Cache miss: pull + save once (~120s), then every subsequent run benefits. --- .github/workflows/test.yaml | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 67428c5..f2f3ddc 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -26,8 +26,43 @@ jobs: - name: Install uv run: curl -LsSf https://astral.sh/uv/install.sh | sh + # Singleuser image is multi-GB and is the dominant cold-start cost + # (~73s pulled in-cluster during the first test). Cache the tar across + # CI runs and side-load it into kind so kubelet never goes to the + # registry. Cache key is the image ref so a `values.yaml` bump + # invalidates automatically. + - name: Resolve singleuser image ref + id: img + run: | + REF=$(yq -r '.jupyterhub.singleuser.image | .name + ":" + .tag' values.yaml) + echo "ref=$REF" >> "$GITHUB_OUTPUT" + echo "tar=/tmp/singleuser.tar" >> "$GITHUB_OUTPUT" + + - name: Cache singleuser image + id: img-cache + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: ${{ steps.img.outputs.tar }} + key: singleuser-${{ steps.img.outputs.ref }} + + - name: Pull + save singleuser image (cache miss only) + if: steps.img-cache.outputs.cache-hit != 'true' + run: | + docker pull "${{ steps.img.outputs.ref }}" + docker save "${{ steps.img.outputs.ref }}" -o "${{ steps.img.outputs.tar }}" + + # Pre-create the cluster here (instead of letting the pytest fixture + # do it) so we can side-load the cached image before any pod is + # scheduled. The fixture's ensure_cluster() reuses an existing one. + - name: Create kind cluster + run: kind create cluster --name nbtest-e2e --wait 60s + + - name: Side-load singleuser image into kind node + run: kind load image-archive "${{ steps.img.outputs.tar }}" --name nbtest-e2e + - name: Run e2e tests env: + KIND_CLUSTER: "nbtest-e2e" # reuse the cluster created above KIND_KEEP: "1" # runner is ephemeral, skip teardown PYTHONUNBUFFERED: "1" # stream logs live (no block buffering on non-TTY) run: uvx pytest tests/e2e -v --color=yes From 1aaa99cbfc03c84edad4e2e6411278bdd364f7c5 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Fri, 1 May 2026 13:18:31 +0100 Subject: [PATCH 23/60] docs(shared-storage): position external RWX as primary, mark in-cluster NFS as transitional MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses comment on issue #29: the bundled `nfsServer.enabled=true` path relies on `quay.io/nebari/volume-nfs:0.8-repack`, a manifest-schema repack of an abandoned upstream image (nebari-dev/nebari-docker-images#230). We should not be carrying that workaround image as the recommended path for a greenfield chart. The chart already supported bringing your own RWX StorageClass; this change makes that path the documented primary: - values.yaml: reframe the sharedStorage block. Recommend an external RWX class with provider-specific examples (Longhorn, EFS, Filestore, Azure Files, nfs-subdir-external-provisioner). Add a deprecation note on the nfsServer.image block linking to issue #29. - README: add a "Shared Storage" section with the same matrix and an explicit pointer to the issue tracking removal of the in-cluster NFS path. No template changes — the external-RWX path was already rendered when nfsServer.enabled=false. Verified via `helm template` that setting only `sharedStorage.storageClass=longhorn` produces a single RWX PVC and no nfs-server pod. --- README.md | 29 +++++++++++++++++++++++++++++ values.yaml | 29 +++++++++++++++++++++++------ 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index b4aef92..4ff3064 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,35 @@ make down See `values.yaml` for all configuration options. The chart wraps the [JupyterHub Helm chart](https://z2jh.jupyter.org/) - all `jupyterhub.*` values are passed through. +## Shared Storage + +Per-group shared directories (`/shared/` in every user pod) need a +ReadWriteMany volume. The chart does **not** ship a storage backend by default +— bring your own RWX `StorageClass` and point the chart at it: + +```yaml +sharedStorage: + enabled: true + storageClass: # e.g. longhorn, efs-sc, azurefile-csi + size: 100Gi +``` + +Recommended options by environment: + +| Environment | RWX backend | Notes | +|-------------|-------------|-------| +| Hetzner / on-prem | [Longhorn](https://longhorn.io/) | Provisioned by NIC's storage layer | +| AWS | [EFS CSI driver](https://github.com/kubernetes-sigs/aws-efs-csi-driver) | RWX via EFS | +| GCP | [Filestore CSI driver](https://cloud.google.com/filestore) | RWX via Filestore | +| Azure | [Azure Files CSI driver](https://github.com/kubernetes-sigs/azurefile-csi-driver) | RWX via Azure Files | +| Generic (have NFS) | [`nfs-subdir-external-provisioner`](https://github.com/kubernetes-sigs/nfs-subdir-external-provisioner) | Point it at any existing NFS export | + +For clusters that cannot provide an RWX class natively, the chart includes a +transitional `sharedStorage.nfsServer.enabled=true` mode that runs an +in-cluster NFS server pod. This is being tracked for removal in +[issue #29](https://github.com/nebari-dev/nebari-data-science-pack/issues/29) — +prefer one of the options above for new deployments. + ## Architecture ``` diff --git a/values.yaml b/values.yaml index 559b705..c3ae1a3 100644 --- a/values.yaml +++ b/values.yaml @@ -73,11 +73,24 @@ singleuserCuller: # Shared storage — per-group directories mounted at /shared/ in user pods. # Requires an RWX volume so all user pods can mount it simultaneously. -# Use nfsServer.enabled=true (below) for an in-cluster NFS solution that works -# on providers with only RWO storage (e.g. Hetzner hcloud-volumes). +# +# RECOMMENDED: bring your own RWX StorageClass and set `storageClass` below. +# Examples by provider: +# - Hetzner / on-prem : longhorn (provisioned by NIC's storage layer) +# - AWS : EFS CSI driver +# - GCP : Filestore CSI driver +# - Azure : Azure Files CSI driver +# - Generic (have NFS) : nfs-subdir-external-provisioner against your NFS export +# +# FALLBACK: `nfsServer.enabled=true` deploys an in-cluster NFS server pod +# (quay.io/nebari/volume-nfs) that re-exports a single RWO PVC as RWX. This +# is a transitional workaround for clusters with no RWX option — see issue +# #29; the image is a repack of an abandoned upstream and we plan to remove +# this code path once an external alternative is documented for every +# supported environment. sharedStorage: enabled: false - # storageClass for the shared PVC when nfsServer.enabled=false. + # StorageClass for the shared PVC when nfsServer.enabled=false. # Leave empty to use the cluster default (must support ReadWriteMany). storageClass: "" size: 10Gi @@ -88,14 +101,18 @@ sharedStorage: groups: [] # Mount path prefix for shared group directories inside user pods. mountPathPrefix: /shared - # In-cluster NFS server — deploys quay.io/nebari/volume-nfs backed by a - # single RWO PVC and re-exports it as RWX NFS to all user pods. - # Enables shared storage on providers that only have RWO StorageClasses. + # Transitional in-cluster NFS server. Prefer an external RWX StorageClass + # (see RECOMMENDED list above). Tracked for removal in issue #29. nfsServer: enabled: false # StorageClass for the NFS server's backend RWO PVC. # Leave empty to use the cluster default. storageClass: "" + # NOTE: 0.8-repack is a manifest-schema repack of the abandoned + # gcr.io/google-containers/nfs-server:0.8 image (see + # nebari-dev/nebari-docker-images#230). We carry it for clusters that + # cannot provide RWX storage natively; new deployments should use + # `sharedStorage.storageClass` instead and leave `nfsServer.enabled=false`. image: repository: quay.io/nebari/volume-nfs tag: "0.8-repack" From b6504f85c678c3ef708d5e5b4d085742c131cd43 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Fri, 1 May 2026 13:26:38 +0100 Subject: [PATCH 24/60] =?UTF-8?q?docs(shared-storage):=20correct=20provide?= =?UTF-8?q?r=20list=20=E2=80=94=20only=20longhorn=20is=20NIC-provisioned?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previous commit listed EFS/Filestore/Azure Files as recommended RWX backends. NIC does not provision those — they are separate cloud-managed services no one in NIC has wired up. NIC's actual storage reality: hetzner : longhorn (longhorn.Install in pkg/provider/hetzner) aws : longhorn (longhorn.Install in pkg/provider/aws) existing : longhorn (longhorn.Install in pkg/provider/existing) gcp : standard-rwo (no RWX provisioned) azure : managed-csi (no RWX provisioned) local : (no storage layer) So the accurate recommendation is just longhorn. Updated values.yaml and README to say so directly. The in-cluster NFS fallback stays — it covers the providers where NIC has not yet wired up an RWX class — with a pointer to issue #29 for tracking removal once that lands everywhere. --- README.md | 27 +++++++++------------------ values.yaml | 27 +++++++++++---------------- 2 files changed, 20 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 4ff3064..15fa974 100644 --- a/README.md +++ b/README.md @@ -61,31 +61,22 @@ See `values.yaml` for all configuration options. The chart wraps the [JupyterHub ## Shared Storage Per-group shared directories (`/shared/` in every user pod) need a -ReadWriteMany volume. The chart does **not** ship a storage backend by default -— bring your own RWX `StorageClass` and point the chart at it: +ReadWriteMany `StorageClass` on the cluster. On NIC-managed clusters that's +[Longhorn](https://longhorn.io/), installed by NIC's storage layer: ```yaml sharedStorage: enabled: true - storageClass: # e.g. longhorn, efs-sc, azurefile-csi + storageClass: longhorn size: 100Gi ``` -Recommended options by environment: - -| Environment | RWX backend | Notes | -|-------------|-------------|-------| -| Hetzner / on-prem | [Longhorn](https://longhorn.io/) | Provisioned by NIC's storage layer | -| AWS | [EFS CSI driver](https://github.com/kubernetes-sigs/aws-efs-csi-driver) | RWX via EFS | -| GCP | [Filestore CSI driver](https://cloud.google.com/filestore) | RWX via Filestore | -| Azure | [Azure Files CSI driver](https://github.com/kubernetes-sigs/azurefile-csi-driver) | RWX via Azure Files | -| Generic (have NFS) | [`nfs-subdir-external-provisioner`](https://github.com/kubernetes-sigs/nfs-subdir-external-provisioner) | Point it at any existing NFS export | - -For clusters that cannot provide an RWX class natively, the chart includes a -transitional `sharedStorage.nfsServer.enabled=true` mode that runs an -in-cluster NFS server pod. This is being tracked for removal in -[issue #29](https://github.com/nebari-dev/nebari-data-science-pack/issues/29) — -prefer one of the options above for new deployments. +For clusters where NIC has not yet wired up an RWX class (local dev, current +GCP/Azure paths), the chart includes a transitional +`sharedStorage.nfsServer.enabled=true` mode that runs an in-cluster NFS +server pod. It depends on the `quay.io/nebari/volume-nfs` workaround image +and is tracked for removal in +[issue #29](https://github.com/nebari-dev/nebari-data-science-pack/issues/29). ## Architecture diff --git a/values.yaml b/values.yaml index c3ae1a3..c71a37c 100644 --- a/values.yaml +++ b/values.yaml @@ -72,22 +72,17 @@ singleuserCuller: shutdownNoActivityTimeout: 900 # 15 min — seconds after last kernel/terminal gone before server self-terminates # Shared storage — per-group directories mounted at /shared/ in user pods. -# Requires an RWX volume so all user pods can mount it simultaneously. +# Requires an RWX StorageClass on the cluster. # -# RECOMMENDED: bring your own RWX StorageClass and set `storageClass` below. -# Examples by provider: -# - Hetzner / on-prem : longhorn (provisioned by NIC's storage layer) -# - AWS : EFS CSI driver -# - GCP : Filestore CSI driver -# - Azure : Azure Files CSI driver -# - Generic (have NFS) : nfs-subdir-external-provisioner against your NFS export +# In NIC-managed clusters that StorageClass is `longhorn`, installed by NIC's +# storage layer (hetzner, aws, existing providers). Set +# `sharedStorage.storageClass: longhorn` and leave `nfsServer.enabled=false`. # -# FALLBACK: `nfsServer.enabled=true` deploys an in-cluster NFS server pod -# (quay.io/nebari/volume-nfs) that re-exports a single RWO PVC as RWX. This -# is a transitional workaround for clusters with no RWX option — see issue -# #29; the image is a repack of an abandoned upstream and we plan to remove -# this code path once an external alternative is documented for every -# supported environment. +# `nfsServer.enabled=true` deploys an in-cluster NFS server pod +# (quay.io/nebari/volume-nfs) that re-exports a RWO PVC as RWX. It exists as +# a transitional fallback for environments where NIC has not yet wired up an +# RWX class (e.g. local dev, GCP, Azure). The image is a manifest-schema +# repack of an abandoned upstream — tracked for removal in issue #29. sharedStorage: enabled: false # StorageClass for the shared PVC when nfsServer.enabled=false. @@ -111,8 +106,8 @@ sharedStorage: # NOTE: 0.8-repack is a manifest-schema repack of the abandoned # gcr.io/google-containers/nfs-server:0.8 image (see # nebari-dev/nebari-docker-images#230). We carry it for clusters that - # cannot provide RWX storage natively; new deployments should use - # `sharedStorage.storageClass` instead and leave `nfsServer.enabled=false`. + # cannot provide RWX storage natively; on NIC-managed clusters set + # `sharedStorage.storageClass: longhorn` and leave nfsServer disabled. image: repository: quay.io/nebari/volume-nfs tag: "0.8-repack" From 5e95e8de2c394725c59344b6229a38100660337e Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Wed, 6 May 2026 20:22:50 +0100 Subject: [PATCH 25/60] fix(nebi-envs): re-fetch auth_state when access token is stale EnvoyOIDCAuthenticator stores no refresh_token (Envoy keeps only access_token + id_token in cookies) and the access_token lifetime is ~5 minutes. jhub-apps calls the env-listing callable on every Create App page render, often well after the token captured at login has expired, producing `token-exchange step 2 FAILED: HTTP 400 invalid_request "Invalid token"` and a silent empty selector. Mirror 01-spawner.py: when access_token has <30s remaining, re-fetch auth_state via the hub API (which refresh_user keeps current with fresh Envoy cookies on browser activity) before exchanging. --- config/jupyterhub/03-nebi-envs.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/config/jupyterhub/03-nebi-envs.py b/config/jupyterhub/03-nebi-envs.py index ff2c086..278f0ed 100644 --- a/config/jupyterhub/03-nebi-envs.py +++ b/config/jupyterhub/03-nebi-envs.py @@ -103,6 +103,26 @@ def get_nebi_environments(user): refresh_token = auth_state.get("refresh_token") access_token = auth_state.get("access_token", "") + # auth_state from EnvoyOIDCAuthenticator carries no refresh_token (Envoy + # only stores access_token + id_token in cookies), and the access_token + # lifetime is short (~5min). When jhub-apps calls this synchronously, the + # access_token may already be expired. Mirror 01-spawner.py: if expiring + # in <30s, re-fetch via the hub API which has the freshest cookie state. + if access_token and not refresh_token: + claims = _decode_jwt_claims(access_token) + import time as _time + exp = claims.get("exp", 0) + remaining = exp - int(_time.time()) if exp else 0 + if remaining < 30: + log.info( + "nebi-envs: access_token for %s expires in %ds, re-fetching auth_state", + username, remaining, + ) + fresh_state = _fetch_fresh_auth_state(username) + if fresh_state: + access_token = fresh_state.get("access_token") or access_token + refresh_token = fresh_state.get("refresh_token") or refresh_token + if not refresh_token and not access_token: log.warning( "nebi-envs: no access_token or refresh_token for user %s, cannot list environments", From 37562126d9792268f028a7648b72acba7a50c3ca Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Wed, 6 May 2026 22:53:42 +0100 Subject: [PATCH 26/60] feat: forward access token via Authorization Bearer header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Envoy Gateway v1.6 stores the OAuth2 access token in the AccessToken cookie at OIDC login but does not rotate the cookie content when its internal refresh token rotates the access token. Result: hub reads a frozen-at-login access token from the cookie and downstream calls that need a fresh JWT (jhub-apps env selector → Keycloak token exchange → Nebi) fail with `400 invalid_request "Invalid token"` ~5 min after login. Three changes: 1. values.yaml — set nebariapp.auth.enforceAtGateway=true and forwardAccessToken=true by default. Envoy Gateway then injects the user's freshly-refreshed access token as `Authorization: Bearer ` on every upstream request. 2. templates/nebariapp.yaml — pass enforceAtGateway, forwardAccessToken, and tokenExchange through to the NebariApp CRD so the chart can drive the operator-managed SecurityPolicy. 3. config/jupyterhub/00-gateway-auth.py — `_extract_envoy_cookies` now prefers `Authorization: Bearer` over the `AccessToken-*` cookie. The header is the only always-current source; cookie fallback retained for deployments without forwardAccessToken. The stale-token re-fetch in 03-nebi-envs.py (commit 5e95e8d) becomes defensive: with this fix, refresh_user captures a fresh access_token on each browser request and the env-listing callable rarely needs to fall back to it. --- config/jupyterhub/00-gateway-auth.py | 18 ++++++++++++++++-- templates/nebariapp.yaml | 10 ++++++++++ values.yaml | 10 ++++++++++ 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/config/jupyterhub/00-gateway-auth.py b/config/jupyterhub/00-gateway-auth.py index e269151..5252a30 100644 --- a/config/jupyterhub/00-gateway-auth.py +++ b/config/jupyterhub/00-gateway-auth.py @@ -44,17 +44,31 @@ def get_handlers(self, app): @staticmethod def _extract_envoy_cookies(handler): - """Extract Envoy Gateway OIDC cookies from the request. + """Extract Envoy Gateway OIDC tokens from the request. Returns (id_token, access_token, refresh_token) — any may be None. + + access_token is preferred from the `Authorization: Bearer` header + because Envoy injects a freshly-refreshed token there per request when + SecurityPolicy.oidc.forwardAccessToken=true. The `AccessToken-*` cookie + content is only updated at OAuth login (Envoy v1.6 doesn't rotate it + on background refresh), so the header is the only always-current + source. Fall back to the cookie when the header is absent (legacy + deployments where forwardAccessToken is off). """ id_token = None access_token = None refresh_token = None + + # Prefer Authorization: Bearer from Envoy. + auth_hdr = handler.request.headers.get("Authorization", "") + if auth_hdr.lower().startswith("bearer "): + access_token = auth_hdr.split(None, 1)[1].strip() or None + for name, value in handler.request.cookies.items(): if name.startswith("IdToken"): id_token = value.value - elif name.startswith("AccessToken"): + elif name.startswith("AccessToken") and access_token is None: access_token = value.value elif name.startswith("RefreshToken"): refresh_token = value.value diff --git a/templates/nebariapp.yaml b/templates/nebariapp.yaml index 3141c38..bcd334b 100644 --- a/templates/nebariapp.yaml +++ b/templates/nebariapp.yaml @@ -25,6 +25,16 @@ spec: scopes: {{- toYaml . | nindent 6 }} {{- end }} + {{- if hasKey . "enforceAtGateway" }} + enforceAtGateway: {{ .enforceAtGateway }} + {{- end }} + {{- if hasKey . "forwardAccessToken" }} + forwardAccessToken: {{ .forwardAccessToken }} + {{- end }} + {{- with .tokenExchange }} + tokenExchange: + {{- toYaml . | nindent 6 }} + {{- end }} {{- end }} {{- with .Values.nebariapp.landingPage }} landingPage: diff --git a/values.yaml b/values.yaml index 2b970e2..7b7cf4d 100644 --- a/values.yaml +++ b/values.yaml @@ -27,6 +27,16 @@ nebariapp: - profile - email - groups + # enforceAtGateway runs the OIDC filter at Envoy Gateway, required by + # forwardAccessToken below. forwardAccessToken makes Envoy inject the + # user's freshly-refreshed access_token as `Authorization: Bearer` on + # every upstream request. Without it, the hub only sees the access_token + # captured at OAuth login — Envoy v1.6 doesn't rotate the AccessToken + # cookie on background refresh — and downstream calls (e.g. Keycloak + # token-exchange for the jhub-apps env selector) fail once it expires + # (~5 min after login). + enforceAtGateway: true + forwardAccessToken: true # landingPage controls whether and how this service appears on the Nebari # landing page (requires nebari-operator with LandingPageConfig support). landingPage: From 61e30b9da88d2f6ed7700e296a51a3f741a72145 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Wed, 6 May 2026 23:23:48 +0100 Subject: [PATCH 27/60] fix(values): default forwardAccessToken=false to avoid jhub-apps loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit forwardAccessToken=true makes Envoy inject the user's Keycloak access token as Authorization: Bearer on every upstream request. jhub-apps's get_current_user reads the Authorization header before its own cookie and unconditionally tries to HS256-decode it as the jhub-apps JWT. The Keycloak token is RS256, decode raises InvalidAlgorithmError, authentication fails, browser is redirected to /jhub-login, OAuth round-trips, new cookie is set, next request hits the same path — infinite loop in the UI. Until jhub-apps is patched to ignore non-HS256 Authorization tokens (or until a different transport delivers the user's fresh access token to the env-listing callable), default to off. The chart still exposes both fields for explicit opt-in. --- values.yaml | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/values.yaml b/values.yaml index 7b7cf4d..b459f22 100644 --- a/values.yaml +++ b/values.yaml @@ -27,16 +27,17 @@ nebariapp: - profile - email - groups - # enforceAtGateway runs the OIDC filter at Envoy Gateway, required by - # forwardAccessToken below. forwardAccessToken makes Envoy inject the - # user's freshly-refreshed access_token as `Authorization: Bearer` on - # every upstream request. Without it, the hub only sees the access_token - # captured at OAuth login — Envoy v1.6 doesn't rotate the AccessToken - # cookie on background refresh — and downstream calls (e.g. Keycloak - # token-exchange for the jhub-apps env selector) fail once it expires - # (~5 min after login). + # enforceAtGateway runs the OIDC filter at Envoy Gateway. Required for + # the operator to manage the SecurityPolicy at all. enforceAtGateway: true - forwardAccessToken: true + # forwardAccessToken would inject the user's fresh access token as + # `Authorization: Bearer` on every upstream request, fixing the + # AccessToken-cookie staleness in Envoy Gateway v1.6. Disabled by default + # because jhub-apps's `get_current_user` reads the Authorization header + # first and tries to decode it as its own HS256 JWT, which fails with + # `InvalidAlgorithmError` and produces an OAuth redirect loop. Re-enable + # once jhub-apps is patched to ignore non-HS256 Authorization tokens. + forwardAccessToken: false # landingPage controls whether and how this service appears on the Nebari # landing page (requires nebari-operator with LandingPageConfig support). landingPage: From e906f78260a0b42424f28a6d0e1df95d87f0a219 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Fri, 8 May 2026 16:02:34 +0200 Subject: [PATCH 28/60] feat: pin jhub-apps to PR #676 + default forwardAccessToken=true Bundle the head of nebari-dev/jhub-apps#676 in the jupyterhub image so the chart can ship with forwardAccessToken=true by default. The upstream patch makes jhub-apps' get_current_user fall through to the cookie when the Authorization header is not its own HS256 wrapper, removing the OAuth redirect-loop that previously forced this default to false. - images/jupyterhub/pixi.toml: pin jhub-apps via git rev (PR #676 head), bump pyjwt to >=2.10 in conda deps to satisfy that branch's requirement. - images/jupyterhub/pixi.lock: regenerated. - values.yaml: nebariapp.auth.forwardAccessToken defaults to true. --- images/jupyterhub/pixi.lock | 6327 +++++++++++++++-------------------- images/jupyterhub/pixi.toml | 8 +- values.yaml | 21 +- 3 files changed, 2653 insertions(+), 3703 deletions(-) diff --git a/images/jupyterhub/pixi.lock b/images/jupyterhub/pixi.lock index 874e92f..1ea2a81 100644 --- a/images/jupyterhub/pixi.lock +++ b/images/jupyterhub/pixi.lock @@ -16,335 +16,281 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/aiosignal-1.3.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/alembic-1.15.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/annotated-types-0.7.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.9.0-pyh29332c3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/appdirs-1.4.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-23.1.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/argon2-cffi-bindings-21.2.0-py310ha75aee5_5.conda - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.3.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.0.5-pyh29332c3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/async-timeout-5.0.1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/async_generator-1.10-pyhd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-25.3.0-pyh71513ae_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.17.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/backports-1.0-pyhd8ed1ab_5.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/backports.tarfile-1.2.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.13.3-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.2.0-pyh29332c3_4.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.2.0-h82add2a_4.conda - conda: https://conda.anaconda.org/conda-forge/noarch/blinker-1.9.0-pyhff2d567_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/bokeh-3.6.3-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/bokeh-root-cmd-0.1.2-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py310hf71b8c6_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.6-hb03c661_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ca-certificates-2025.1.31-hbcca054_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cachecontrol-0.14.2-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cachecontrol-with-filecache-0.14.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/cachetools-5.5.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cachy-0.3.0-pyhd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.1.31-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/certipy-0.2.1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-1.17.1-py310h8deb56e_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/click-8.1.8-pyh707e725_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/click-default-group-1.2.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/clikit-0.6.2-pyhd8ed1ab_3.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/conda-lock-2.5.7-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/conda-project-0.4.2-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/configurable-http-proxy-4.6.3-hbf95b10_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/contourpy-1.3.1-py310h3788b33_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/crashtest-0.4.1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cryptography-44.0.2-py310h6c63255_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/curl-8.18.0-h4e3cde8_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/dbus-1.13.6-h5008d03_3.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/debugpy-1.8.13-py310hf71b8c6_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/distlib-0.3.9-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/dnspython-2.7.0-pyhff2d567_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/durationpy-0.9-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/editables-0.5-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/email-validator-2.2.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/email_validator-2.2.0-hd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ensureconda-1.4.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/escapism-1.0.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.1.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/expat-2.7.3-hecca717_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/fastapi-0.115.11-pyh29332c3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/fastapi-cli-0.0.7-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/filelock-3.18.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/freetype-2.13.3-h48d6fc4_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/frozenlist-1.5.0-py310h89163eb_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/fsspec-2025.3.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/gettext-0.25.1-h3f43e3d_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/gettext-tools-0.25.1-h3f43e3d_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/git-2.52.0-pl5321h6d3cee1_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.12-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.44-pyhff2d567_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/google-auth-2.38.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/greenlet-3.1.1-py310hf71b8c6_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.14.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/hatch-1.14.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/hatchling-1.27.0-pypyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/html5lib-1.1-pyhd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.7-pyh29332c3_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/httptools-0.6.4-py310ha75aee5_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/hyperlink-21.0.0-pyh29332c3_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.6.1-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.5.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-6.29.5-pyh3099207_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-8.34.0-pyh907856f_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ipywidgets-8.1.5-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jaraco.classes-3.4.0-pyhd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jaraco.context-6.0.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jaraco.functools-4.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jeepney-0.9.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jhub-apps-2025.11.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/json5-0.10.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/jsonpointer-3.0.0-py310hff52083_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.23.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2024.10.1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.23.0-hd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-1.1.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.2.5-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.6.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_console-6.6.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.7.2-pyh31011fe_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.0-pyh29332c3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.15.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterhub-5.1.0-pyh31011fe_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterhub-base-5.1.0-pyh31011fe_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterhub-idle-culler-1.2.1-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterhub-kubespawner-6.2.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.3.6-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.27.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_widgets-3.0.13-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/keyring-25.6.0-pyha804496_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.1-h166bdaf_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.3-h659f571_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/kubernetes_asyncio-29.0.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/lcms2-2.17-h717163a_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.43-h712a8e2_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/lerc-4.0.0-h27087fc_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/libarchive-3.7.7-h4585015_3.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libasprintf-0.25.1-h3f43e3d_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libasprintf-devel-0.25.1-h3f43e3d_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-31_h59b9bed_openblas.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-31_he106b2a_openblas.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.18.0-h4e3cde8_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.23-h4ddbbb0_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.3-hecca717_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-14.2.0-h767d61c_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-14.2.0-h69a702a_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgettextpo-0.25.1-h3f43e3d_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgettextpo-devel-0.25.1-h3f43e3d_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran-14.2.0-h69a702a_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-14.2.0-hf1ad2bd_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libglib-2.72.1-h2d90d5f_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-14.2.0-h767d61c_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h4ce23a2_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libidn2-2.3.8-hfac485b_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.0.0-hd590300_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-31_h7ac8fdf_openblas.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.6.4-hb9d3cd8_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.67.0-had1ee68_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hd590300_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.29-pthreads_h94d23a6_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.47-h943b412_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libsodium-1.0.20-h4ab18f5_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.49.1-hee588c1_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.1-hf672d98_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-14.2.0-h8f9b012_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-14.2.0-h4852527_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.7.0-hd9ff511_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libunistring-0.9.10-h7f98852_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libuv-1.50.0-hb9d3cd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.5.0-h851e524_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.17.0-h8a09558_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.13.6-h8d12d68_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/linkify-it-py-2.0.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/lockfile-0.12.2-py_1.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.10.0-h5888daf_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/lzo-2.10-hd590300_1001.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mako-1.3.9-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-3.6-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-3.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.2-py310h89163eb_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.1.7-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mdit-py-plugins-0.4.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.1.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/more-itertools-10.6.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/msgpack-python-1.1.0-py310h3788b33_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/multidict-6.2.0-py310h89163eb_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.16.6-pyh29332c3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/nodejs-22.13.0-hf235a45_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-7.3.3-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/numpy-2.2.4-py310hefbff90_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/oauthenticator-16.3.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/oauthlib-3.2.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/openjpeg-2.5.3-h5fbd93e_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.1-h35e630c_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pamela-1.2.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/pandas-2.2.3-py310h5eaa309_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/panel-1.6.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/param-2.2.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pastel-0.2.1-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-0.12.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/pcre-8.45-h9c3ff4c_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.47-haa7fec5_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/perl-5.32.1-7_hd590300_perl5.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pickleshare-0.7.5-pyhd8ed1ab_1004.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/pillow-11.1.0-py310h7e6dc6c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pkginfo-1.12.1.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pkgutil-resolve-name-1.3.10-pyhd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.3.6-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/plotlydash-tornado-cmd-0.0.6-pyhd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.21.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.50-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/prompt_toolkit-3.0.50-hd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/propcache-0.2.1-py310h89163eb_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.0.0-py310ha75aee5_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-hb9d3cd8_1002.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pyasn1-0.6.1-pyhd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pyasn1-modules-0.4.1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pycurl-7.45.6-py310h6811363_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.10.6-pyh3cfb1c2_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pydantic-core-2.27.2-py310h505e2c1_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pyjwt-2.9.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pylev-1.4.0-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/pyjwt-2.12.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pyopenssl-25.0.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.10.16-he725a3c_1_cpython.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhff2d567_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-dotenv-1.0.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-2.0.7-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-kubernetes-32.0.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-libarchive-c-5.2-pyh29332c3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-multipart-0.0.20-pyhff2d567_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-slugify-8.0.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.10-5_cp310.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2024.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pyu2f-0.1.5-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pyviz_comms-3.0.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.2-py310h89163eb_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/pyzmq-26.3.0-py310h71f11fc_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.36.2-pyh29332c3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/requests-oauthlib-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/rich-13.9.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/rich-toolkit-0.11.3-pyh29332c3_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-0.23.1-py310hc1293b2_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rsa-4.9-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ruamel.yaml-0.18.10-py310ha75aee5_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ruamel.yaml.clib-0.2.8-py310ha75aee5_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/secretstorage-3.3.3-py310hff52083_3.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-1.8.3-pyh0d859eb_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-75.8.2-pyhff2d567_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/shellingham-1.5.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/smmap-5.0.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.5-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/sqlalchemy-1.4.46-py310h1fa729e_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/starlette-0.46.1-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/structlog-25.2.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh0d859eb_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/text-unidecode-1.3-pyhd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-w-1.2.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.13.2-pyha770c72_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/toolz-0.12.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.4.2-py310ha75aee5_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.67.1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/trove-classifiers-2025.3.13.13-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typer-0.15.2-pyhff008b6_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typer-slim-0.15.2-pyh29332c3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typer-slim-standard-0.15.2-h801b22e_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/types-python-dateutil-2.9.0.20241206-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.12.2-hd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.12.2-pyha770c72_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing_utils-0.1.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025a-h78e105d_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/uc-micro-py-1.0.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-1.26.19-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/userpath-1.9.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/uv-0.6.7-h0f3a69f_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/uvicorn-0.34.0-pyh31011fe_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/uvicorn-standard-0.34.0-h31011fe_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/uvloop-0.21.0-py310ha75aee5_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/virtualenv-20.29.3-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/watchfiles-1.0.4-py310h505e2c1_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.2.13-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-24.11.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.8.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/websockets-15.0.1-py310ha75aee5_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/wget-1.21.4-hda4d442_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/widgetsnbextension-4.0.13-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxau-1.0.12-hb9d3cd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdmcp-1.1.5-hb9d3cd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/xyzservices-2025.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h7f98852_2.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-64/yarl-1.18.3-py310h89163eb_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/zeromq-4.3.5-h3b0a872_7.conda - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.21.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/zlib-1.3.1-hb9d3cd8_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.23.0-py310ha75aee5_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_1.conda + - pypi: https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3b/00/2344469e2084fb287c2e0b57b72910309874c3245463acd6cf5e3db69324/appdirs-1.4.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/09/52/94108adfdd6e2ddf58be64f959a0b9c7d4ef2fa71086c38356d22dc501ea/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e5/e2/c2e3abf398f80732e58b03be77bde9022550d221dd8781bf586bd4d97cc1/async_lru-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ea/cf/7fc0d8c3b5bae488ddab183a203474d9066fe77b0b563b3a345fc141ac50/backports_zstd-1.4.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cd/3a/577b549de0cc09d95f11087ee63c739bba856cd3952697eec4c4bb91350a/bleach-6.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/47/0b/bdf449df87be3f07b23091ceafee8c3ef569cf6d2fb7edec6e3b12b3faa4/bokeh-3.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/92/60/aec8c2f63f49a5e3064362d31d37c0ab2a43b0dc3e093e11ca0034f4cf49/bokeh_root_cmd-0.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/45/7f/0e961cf3908bc4c1c3e027de2794f867c6c89fb4916fc7dba295a0e80a2d/boltons-25.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ef/79/c45f2d53efe6ada1110cf6f9fca095e4ff47a0454444aefdde6ac4789179/cachecontrol-0.14.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2c/1a/aff8bb287a4b1400f69e09a53bd65de96aa5cee5691925b38731c67fc695/click_default_group-1.2.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/31/28/237390aa8425404fbe15dbca65e724e1b3ac28be9215568c25d455f53111/conda_lock-4.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/87/39/0bd20aa98ae43756b0bb5d8b055c19de842b422fe588eaa16f11dce1f399/conda_package_streaming-0.12.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f3/3e/a548d42e64c4928ae60b03339c955ad115fdc5d049cdaf07112b789d1862/conda_project-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/32/5c/1ee32d1c7956923202f00cf8d2a14a62ed7517bdc0ee1e55301227fc273c/contourpy-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/b0/5c/3ba7d12e7a79566f97b8f954400926d7b6eb33bcdccc1315a857f200f1f1/crashtest-0.4.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e0/c3/7f67dea8ccf8fdcb9c99033bbe3e90b9e7395415843accb81428c441be2d/debugpy-1.8.20-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b9/f9/c248185bf982f2efbc02f1de5783b0e4aae94975b07d293e2aae1901c4b1/dulwich-0.24.1-cp310-cp310-manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/cb/a3/460c57f094a4a165c84a1341c373b0a4f5ec6ac244b998d5021aade89b77/ecdsa-0.19.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f5/64/82570019b199a85b1d9559e75f448272fc6a79201d8692a1357f3f803837/ensureconda-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5a/ff/2e4eca3ade2c22fe1dea7043b8ee9dabe47753349eb1b56a202de8af6349/fastapi-0.136.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6b/29/742010d61a7665b863a36208bfa3df93476e9a86fde45413cd13db76f7d0/hatch-1.16.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d3/8a/44032265776062a89171285ede55a0bdaadc8ac00f27f0512a71a9e3e1c8/hatchling-1.29.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6e/aa/8caf6a0a3e62863cbb9dab27135660acba46903b703e224f14f447e57934/hyperlink-21.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e5/ca/1172b6638d52f2d6caa2dd262ec4c811ba59eee96d54a7701930726bce18/installer-0.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/82/b9/e73d5d9f405cba7706c539aa8b311b49d4c2f3d698d9c12f815231169c71/ipykernel-7.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c0/56/4cc7fc9e9e3f38fd324f24f8afe0ad8bb5fa41283f37f1aaf9de0612c968/ipython-8.39.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9a/93/242e2eab5fe682ffcb8b0084bde703a41d51e17ee0f3a31ff0d9d813620a/jedi-0.20.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl + - pypi: git+https://github.com/nebari-dev/jhub-apps.git?rev=a94b049a9bcc72e2844d5cf0cc2978d4badd2cd5#a94b049a9bcc72e2844d5cf0cc2978d4badd2cd5 + - pypi: https://files.pythonhosted.org/packages/b8/42/cf027b4ac873b076189d935b135397675dac80cb29acb13e1ab86ad6c631/json5-0.14.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/64/285f20a31679bf547b75602702f7800e74dbabae36ef324f716c02804753/jupyter-1.1.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2d/0b/ceb7694d864abc0a047649aec263878acb9f792e1fec3e676f22dc9015e3/jupyter_client-8.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ca/77/71d78d58f15c22db16328a476426f7ac4a60d3a5a7ba3b9627ee2f7903d4/jupyter_console-6.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/23/e8/9d61dcbd1dce8ef418f06befd4ac084b4720429c26b0b1222bc218685eff/jupyter_lsp-2.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e2/50/ecf4f70d65bdb7519b28a33d1b2fee8a4b4ba1ae1a92f15d97e877c5de21/jupyter_server-2.18.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d1/2d/6674563f71c6320841fc300911a55143925112a72a883e2ca71fba4c618d/jupyter_server_terminals-0.5.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3d/aa/537b8f7d80e799af19af35fb3ddfc970b951088a13c57dd9387dcfbb7f61/jupyterlab-4.5.7-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/3f/ff00c588ebd7eae46a9d6223389f5ae28a3af4b6d975c0f2a6d86b1342b9/libarchive_c-5.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b4/de/88b3be5c31b22333b3ca2f6ff1de4e863d8fe45aaea7485f591970ec1d3e/linkify_it_py-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c8/22/9460e311f340cb62d26a38c419b1381b8593b0bb6b5d1f056938b086d362/lockfile-0.12.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/71/d6/48f5b9e44e2e760855d7b489b1317cd7620e82dcb73197961e5cc1391348/mdit_py_plugins-0.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2a/7f/a946aa4f8752b37102b41e64dca18a1976ac705c3a0d1dfe74d820a02552/mistune-3.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cb/98/6af411189d9413534c3eb691182bff1f5c6d44ed2f93f2edfe52a1bbceb8/more_itertools-11.0.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/09/2a06956383c0fdebaef5aa9246e2356776f12ea6f2a44bd1368abf0e46c4/msgpack-1.1.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/c7/e1/68c2256b69a314eba133673377ba9118c356f6342a0c02b61de449cf2bf2/narwhals-2.21.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/83/a0/5b0c2f11142ed1dddec842457d3f65eaf71a0080894eb6f018755b319c3a/nbclient-0.10.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/67/f8/bb0a9d5f46819c821dc1f004aa2cc29b1d91453297dbf5ff20470f00f193/nbconvert-7.17.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d3/84/a2d7ced096d0c0c16d663fec2da68a13312e2b621fad19fe324f943c1f55/nebari_jupyterhub_theme-2024.7.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e9/d6/1fd0646b9bbd9efbb0b8ae21b2325fbef515769a5621c03e31d8eb8da587/notebook-7.5.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/2c/ab/fc8290c6a4c722e5514d80f62b2dc4c4df1a68a41d1364e625c35990fcf3/overrides-7.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/40/a8/4dac1f8f8235e5d25b9955d02ff6f29396191d4e665d71122c3722ca83c5/pandas-2.3.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/58/85/3290cc84bb35503293ea23d2a0b39a78cf02c560ae1455502b042975c951/panel-1.8.10-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ea/2b/4bc14b04ae260e68c4db7d1e5d2fdda214fb4381236fc8e2d720ed14e2d9/param-2.3.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/5d/8268b644392ee874ee82a635cd0df1773de230bde356c38de28e298392cc/parso-0.8.7-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b5/42/bbcb6051030e1e421d103ce7a8ecadf837aa2f39b8f82ef1a8d37c3d4ebc/pillow-12.2.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/fa/3d/f4f2ba829efb54b6cd2d91349c7463316a9cc55a43fc980447416c88540f/pkginfo-1.12.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/29/1c/1b83ffb56adb76abe46df91439e9a613bba45e4612a4e6a7bbebca0b35bb/plotlydash_tornado_cmd-0.0.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/41/4a/755a692976425af4627ffba016dd0576a045c70066553a27cc4023d44d58/python-keycloak-0.26.1.tar.gz + - pypi: https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bd/2d/e94b2f7bab6773c70efc70a61d66e312e1febccd9e0db6b9e0adf58cbad1/python_jose-3.3.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/78/4126abcbdbd3c559d43e0db7f7b9173fc6befe45d39a2856cc0b8ec2a5a6/python_multipart-0.0.27-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/dd/96da98f892250475bdf2328112d7468abdd4acc7b902b6af23f4ed958ea0/pytz-2026.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/13/5a/f8c0868199bbb231a02616286ce8a4ccb85f5387b9215510297dcfedd214/pyviz_comms-3.0.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/30/76/8f099f9d6482450428b17c4d6b241281af7ce6a9de8149ca8c1c649f6792/pyzmq-27.1.0-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a6/24/4d91e05817e92e3a61c8a21e08fd0f390f5301f1c448b137c57c4bc6e543/semver-3.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1c/78/504fdd027da3b84ff1aecd9f6957e65f35134534ccc6da8628eb71e76d3f/send2trash-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a8/45/a132b9074aa18e799b891b91ad72133c98d8042c70f6240e4c5f9dabee2f/structlog-25.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b5/11/87d6d29fb5d237229d67973a6c9e06e048f01cf4994dee194ab0ea841814/tomlkit-0.14.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7b/e3/d81b065a2d866a33a541ac63a2a4cc5737e03ce2379ac3191c98bb8867e3/trove_classifiers-2026.5.7.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/61/73/d21edf5b204d1467e06500080a50f79d49ef2b997c79123a536d4a17d97c/uc_micro_py-2.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/43/99/3ec6335ded5b88c2f7ed25c56ffd952546f7ed007ffb1e1539dc3b57015a/userpath-1.9.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4c/f8/a5d5bac297b1379719050788c6b852c6b3eefcb1e82d8465ed22c10cede7/uv-0.11.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/31/a3/5b1562db76a5a488274b2332a97199b32d0442aca0ed193697fd47786316/uvicorn-0.46.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ca/ff/ded57ac5ff40a09e6e198550bab075d780941e0b0f83cbeabd087c59383a/virtualenv-20.33.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/41/52/e465037f5375f43533d1a80b6923955201596a99142ed524d77b571a1418/wcwidth-0.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a8/a9/d23012099dc88ec69a29c6407b41d89681cb674c2043cd5b467c7e299c08/xyzservices-2026.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fd/e5/6d36f92a197c3c17729a2125e29c169f460538a7d939a27eaaa6dcfcba8e/zstandard-0.25.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl linux-aarch64: - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/_openmp_mutex-4.5-2_gnu.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/aiohappyeyeballs-2.6.1-pyhd8ed1ab_0.conda @@ -352,335 +298,281 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/aiosignal-1.3.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/alembic-1.15.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/annotated-types-0.7.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.9.0-pyh29332c3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/appdirs-1.4.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-23.1.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/argon2-cffi-bindings-21.2.0-py310ha766c32_5.conda - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.3.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.0.5-pyh29332c3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/async-timeout-5.0.1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/async_generator-1.10-pyhd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-25.3.0-pyh71513ae_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.17.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/backports-1.0-pyhd8ed1ab_5.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/backports.tarfile-1.2.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.13.3-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.2.0-pyh29332c3_4.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.2.0-h82add2a_4.conda - conda: https://conda.anaconda.org/conda-forge/noarch/blinker-1.9.0-pyhff2d567_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/bokeh-3.6.3-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/bokeh-root-cmd-0.1.2-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/brotli-python-1.1.0-py310he30c3ed_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/bzip2-1.0.8-h68df207_7.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/c-ares-1.34.6-he30d5cf_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ca-certificates-2025.1.31-hcefe29a_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cachecontrol-0.14.2-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cachecontrol-with-filecache-0.14.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/cachetools-5.5.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/cachy-0.3.0-pyhd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.1.31-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/certipy-0.2.1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cffi-1.17.1-py310h1451162_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/click-8.1.8-pyh707e725_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/click-default-group-1.2.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/clikit-0.6.2-pyhd8ed1ab_3.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/conda-lock-2.5.7-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/conda-project-0.4.2-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/configurable-http-proxy-4.6.3-h0ee932a_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/contourpy-1.3.1-py310hf54e67a_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/crashtest-0.4.1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cryptography-44.0.2-py310h42c23b7_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/curl-8.18.0-h7bfdcfb_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/dbus-1.13.6-h12b9eeb_3.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/debugpy-1.8.13-py310he30c3ed_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/distlib-0.3.9-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/dnspython-2.7.0-pyhff2d567_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/durationpy-0.9-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/editables-0.5-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/email-validator-2.2.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/email_validator-2.2.0-hd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ensureconda-1.4.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/escapism-1.0.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.1.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/expat-2.7.3-hfae3067_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/fastapi-0.115.11-pyh29332c3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/fastapi-cli-0.0.7-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/filelock-3.18.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/freetype-2.13.3-he93130f_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/frozenlist-1.5.0-py310heeae437_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/fsspec-2025.3.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gettext-0.25.1-h5ad3122_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gettext-tools-0.25.1-h5ad3122_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/git-2.52.0-pl5321h5dcfaa0_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.12-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.44-pyhff2d567_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/google-auth-2.38.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/greenlet-3.1.1-py310he30c3ed_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.14.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/hatch-1.14.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/hatchling-1.27.0-pypyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/html5lib-1.1-pyhd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.7-pyh29332c3_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/httptools-0.6.4-py310ha766c32_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/hyperlink-21.0.0-pyh29332c3_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/icu-75.1-hf9b3779_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.6.1-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.5.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-6.29.5-pyh3099207_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-8.34.0-pyh907856f_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ipywidgets-8.1.5-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jaraco.classes-3.4.0-pyhd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jaraco.context-6.0.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jaraco.functools-4.1.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jeepney-0.9.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jhub-apps-2025.11.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/json5-0.10.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/jsonpointer-3.0.0-py310h4c7bcd0_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.23.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2024.10.1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.23.0-hd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-1.1.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.2.5-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.6.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_console-6.6.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.7.2-pyh31011fe_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.0-pyh29332c3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.15.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterhub-5.1.0-pyh31011fe_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterhub-base-5.1.0-pyh31011fe_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterhub-idle-culler-1.2.1-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterhub-kubespawner-6.2.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.3.6-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.27.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_widgets-3.0.13-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/keyring-25.6.0-pyha804496_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/keyutils-1.6.1-h4e544f5_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/krb5-1.21.3-h50a48e9_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/kubernetes_asyncio-29.0.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/lcms2-2.17-hc88f144_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ld_impl_linux-aarch64-2.43-h80caac9_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/lerc-4.0.0-h4de3ea5_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libarchive-3.7.7-h6223a6c_3.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libasprintf-0.25.1-h5e0f5ae_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libasprintf-devel-0.25.1-h5e0f5ae_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libblas-3.9.0-31_h1a9f1db_openblas.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libcblas-3.9.0-31_hab92f65_openblas.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libcurl-8.18.0-h7bfdcfb_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libdeflate-1.23-h5e3c512_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libedit-3.1.20250104-pl5321h976ea20_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libev-4.33-h31becfc_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libexpat-2.7.3-hfae3067_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libffi-3.4.6-he21f813_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgcc-14.2.0-he277a41_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgcc-ng-14.2.0-he9431aa_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgettextpo-0.25.1-h5ad3122_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgettextpo-devel-0.25.1-h5ad3122_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgfortran-14.2.0-he9431aa_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgfortran5-14.2.0-hb6113d0_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libglib-2.72.1-hd4f7528_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgomp-14.2.0-he277a41_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libiconv-1.18-hc99b53d_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libidn2-2.3.8-h99ff5a0_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libjpeg-turbo-3.0.0-h31becfc_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/liblapack-3.9.0-31_h411afd4_openblas.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/liblzma-5.6.4-h86ecc28_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libnghttp2-1.67.0-ha888d0e_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libnsl-2.0.1-h31becfc_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libopenblas-0.3.29-pthreads_h9d3fd7e_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libpng-1.6.47-hec79eb8_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libsodium-1.0.20-h68df207_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libsqlite-3.49.1-h5eb1b54_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libssh2-1.11.1-ha41c0db_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libstdcxx-14.2.0-h3f4de04_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libstdcxx-ng-14.2.0-hf1166c9_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libtiff-4.7.0-h88f7998_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libunistring-0.9.10-hf897c2e_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libuuid-2.38.1-hb4cce97_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libuv-1.50.0-h86ecc28_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libwebp-base-1.5.0-h0886dbf_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libxcb-1.17.0-h262b8f6_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libxcrypt-4.4.36-h31becfc_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libxml2-2.13.6-h2e0c361_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libzlib-1.3.1-h86ecc28_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/linkify-it-py-2.0.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/lockfile-0.12.2-py_1.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/lz4-c-1.10.0-h5ad3122_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/lzo-2.10-h31becfc_1001.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mako-1.3.9-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-3.6-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-3.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/markupsafe-3.0.2-py310h66848f9_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.1.7-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mdit-py-plugins-0.4.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.1.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/more-itertools-10.6.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/msgpack-python-1.1.0-py310hf54e67a_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/multidict-6.2.0-py310heeae437_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.16.6-pyh29332c3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ncurses-6.5-ha32ae93_3.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/nodejs-22.13.0-h8374285_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-7.3.3-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/numpy-2.2.4-py310h6e5608f_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/oauthenticator-16.3.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/oauthlib-3.2.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/openjpeg-2.5.3-h3f56577_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/openssl-3.6.1-h546c87b_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pamela-1.2.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pandas-2.2.3-py310hce852f7_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/panel-1.6.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/param-2.2.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pastel-0.2.1-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-0.12.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pcre-8.45-h01db608_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pcre2-10.47-hf841c20_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/perl-5.32.1-7_h31becfc_perl5.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pickleshare-0.7.5-pyhd8ed1ab_1004.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pillow-11.1.0-py310h34c99de_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pkginfo-1.12.1.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pkgutil-resolve-name-1.3.10-pyhd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.3.6-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/plotlydash-tornado-cmd-0.0.6-pyhd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.21.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.50-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/prompt_toolkit-3.0.50-hd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/propcache-0.2.1-py310heeae437_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/psutil-7.0.0-py310ha766c32_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pthread-stubs-0.4-h86ecc28_1002.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pyasn1-0.6.1-pyhd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pyasn1-modules-0.4.1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pycurl-7.45.6-py310h25e8102_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.10.6-pyh3cfb1c2_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pydantic-core-2.27.2-py310h04a307d_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pyjwt-2.9.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pylev-1.4.0-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/pyjwt-2.12.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pyopenssl-25.0.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/python-3.10.16-h57b00e1_1_cpython.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhff2d567_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-dotenv-1.0.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-2.0.7-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-kubernetes-32.0.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-libarchive-c-5.2-pyh29332c3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-multipart-0.0.20-pyhff2d567_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-slugify-8.0.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/python_abi-3.10-5_cp310.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2024.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pyu2f-0.1.5-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pyviz_comms-3.0.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pyyaml-6.0.2-py310heeae437_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pyzmq-26.3.0-py310h55e1596_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/readline-8.2-h8382b9d_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.36.2-pyh29332c3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/requests-oauthlib-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/rich-13.9.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/rich-toolkit-0.11.3-pyh29332c3_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/rpds-py-0.23.1-py310h2839ab5_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rsa-4.9-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ruamel.yaml-0.18.10-py310ha766c32_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ruamel.yaml.clib-0.2.8-py310ha766c32_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/secretstorage-3.3.3-py310hbbe02a8_3.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-1.8.3-pyh0d859eb_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-75.8.2-pyhff2d567_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/shellingham-1.5.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/smmap-5.0.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.5-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/sqlalchemy-1.4.46-py310h734f5e8_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/starlette-0.46.1-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/structlog-25.2.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh0d859eb_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/text-unidecode-1.3-pyhd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/tk-8.6.13-h194ca79_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-w-1.2.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.13.2-pyha770c72_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/toolz-0.12.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/tornado-6.4.2-py310h78583b1_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.67.1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/trove-classifiers-2025.3.13.13-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typer-0.15.2-pyhff008b6_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typer-slim-0.15.2-pyh29332c3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typer-slim-standard-0.15.2-h801b22e_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/types-python-dateutil-2.9.0.20241206-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.12.2-hd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.12.2-pyha770c72_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing_utils-0.1.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025a-h78e105d_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/uc-micro-py-1.0.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-1.26.19-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/userpath-1.9.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/uv-0.6.7-h2016286_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/uvicorn-0.34.0-pyh31011fe_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/uvicorn-standard-0.34.0-h31011fe_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/uvloop-0.21.0-py310ha766c32_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/virtualenv-20.29.3-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/watchfiles-1.0.4-py310h04a307d_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.2.13-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-24.11.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.8.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/websockets-15.0.1-py310ha766c32_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/wget-1.21.4-h3861a24_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/widgetsnbextension-4.0.13-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxau-1.0.12-h86ecc28_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxdmcp-1.1.5-h57736b2_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/xyzservices-2025.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/yaml-0.2.5-hf897c2e_2.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/yarl-1.18.3-py310heeae437_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/zeromq-4.3.5-h5efb499_7.conda - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.21.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/zlib-1.3.1-h86ecc28_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/zstandard-0.23.0-py310ha766c32_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/zstd-1.5.7-hbcf94c1_1.conda + - pypi: https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3b/00/2344469e2084fb287c2e0b57b72910309874c3245463acd6cf5e3db69324/appdirs-1.4.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c1/93/44365f3d75053e53893ec6d733e4a5e3147502663554b4d864587c7828a7/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e5/e2/c2e3abf398f80732e58b03be77bde9022550d221dd8781bf586bd4d97cc1/async_lru-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/90/a7/a521ea420aded33ca9821b3ef98e5bbdbd78d5d41711684cfce1a5d127a8/backports_zstd-1.4.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cd/3a/577b549de0cc09d95f11087ee63c739bba856cd3952697eec4c4bb91350a/bleach-6.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/47/0b/bdf449df87be3f07b23091ceafee8c3ef569cf6d2fb7edec6e3b12b3faa4/bokeh-3.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/92/60/aec8c2f63f49a5e3064362d31d37c0ab2a43b0dc3e093e11ca0034f4cf49/bokeh_root_cmd-0.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/45/7f/0e961cf3908bc4c1c3e027de2794f867c6c89fb4916fc7dba295a0e80a2d/boltons-25.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ef/79/c45f2d53efe6ada1110cf6f9fca095e4ff47a0454444aefdde6ac4789179/cachecontrol-0.14.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2c/1a/aff8bb287a4b1400f69e09a53bd65de96aa5cee5691925b38731c67fc695/click_default_group-1.2.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/31/28/237390aa8425404fbe15dbca65e724e1b3ac28be9215568c25d455f53111/conda_lock-4.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/87/39/0bd20aa98ae43756b0bb5d8b055c19de842b422fe588eaa16f11dce1f399/conda_package_streaming-0.12.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f3/3e/a548d42e64c4928ae60b03339c955ad115fdc5d049cdaf07112b789d1862/conda_project-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c1/bd/20c6726b1b7f81a8bee5271bed5c165f0a8e1f572578a9d27e2ccb763cb2/contourpy-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/b0/5c/3ba7d12e7a79566f97b8f954400926d7b6eb33bcdccc1315a857f200f1f1/crashtest-0.4.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e0/c3/7f67dea8ccf8fdcb9c99033bbe3e90b9e7395415843accb81428c441be2d/debugpy-1.8.20-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5e/a6/c13168490a926f84447e9035fb5d10504d7c4d81a68162a900c59b1fc0fe/dulwich-0.24.1-cp310-cp310-manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/cb/a3/460c57f094a4a165c84a1341c373b0a4f5ec6ac244b998d5021aade89b77/ecdsa-0.19.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f5/64/82570019b199a85b1d9559e75f448272fc6a79201d8692a1357f3f803837/ensureconda-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5a/ff/2e4eca3ade2c22fe1dea7043b8ee9dabe47753349eb1b56a202de8af6349/fastapi-0.136.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6b/29/742010d61a7665b863a36208bfa3df93476e9a86fde45413cd13db76f7d0/hatch-1.16.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d3/8a/44032265776062a89171285ede55a0bdaadc8ac00f27f0512a71a9e3e1c8/hatchling-1.29.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6e/aa/8caf6a0a3e62863cbb9dab27135660acba46903b703e224f14f447e57934/hyperlink-21.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e5/ca/1172b6638d52f2d6caa2dd262ec4c811ba59eee96d54a7701930726bce18/installer-0.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/82/b9/e73d5d9f405cba7706c539aa8b311b49d4c2f3d698d9c12f815231169c71/ipykernel-7.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c0/56/4cc7fc9e9e3f38fd324f24f8afe0ad8bb5fa41283f37f1aaf9de0612c968/ipython-8.39.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9a/93/242e2eab5fe682ffcb8b0084bde703a41d51e17ee0f3a31ff0d9d813620a/jedi-0.20.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl + - pypi: git+https://github.com/nebari-dev/jhub-apps.git?rev=a94b049a9bcc72e2844d5cf0cc2978d4badd2cd5#a94b049a9bcc72e2844d5cf0cc2978d4badd2cd5 + - pypi: https://files.pythonhosted.org/packages/b8/42/cf027b4ac873b076189d935b135397675dac80cb29acb13e1ab86ad6c631/json5-0.14.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/64/285f20a31679bf547b75602702f7800e74dbabae36ef324f716c02804753/jupyter-1.1.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2d/0b/ceb7694d864abc0a047649aec263878acb9f792e1fec3e676f22dc9015e3/jupyter_client-8.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ca/77/71d78d58f15c22db16328a476426f7ac4a60d3a5a7ba3b9627ee2f7903d4/jupyter_console-6.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/23/e8/9d61dcbd1dce8ef418f06befd4ac084b4720429c26b0b1222bc218685eff/jupyter_lsp-2.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e2/50/ecf4f70d65bdb7519b28a33d1b2fee8a4b4ba1ae1a92f15d97e877c5de21/jupyter_server-2.18.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d1/2d/6674563f71c6320841fc300911a55143925112a72a883e2ca71fba4c618d/jupyter_server_terminals-0.5.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3d/aa/537b8f7d80e799af19af35fb3ddfc970b951088a13c57dd9387dcfbb7f61/jupyterlab-4.5.7-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/3f/ff00c588ebd7eae46a9d6223389f5ae28a3af4b6d975c0f2a6d86b1342b9/libarchive_c-5.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b4/de/88b3be5c31b22333b3ca2f6ff1de4e863d8fe45aaea7485f591970ec1d3e/linkify_it_py-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c8/22/9460e311f340cb62d26a38c419b1381b8593b0bb6b5d1f056938b086d362/lockfile-0.12.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/71/d6/48f5b9e44e2e760855d7b489b1317cd7620e82dcb73197961e5cc1391348/mdit_py_plugins-0.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2a/7f/a946aa4f8752b37102b41e64dca18a1976ac705c3a0d1dfe74d820a02552/mistune-3.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cb/98/6af411189d9413534c3eb691182bff1f5c6d44ed2f93f2edfe52a1bbceb8/more_itertools-11.0.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/71/e5/c2241de64bfceac456b140737812a2ab310b10538a7b34a1d393b748e095/msgpack-1.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/c7/e1/68c2256b69a314eba133673377ba9118c356f6342a0c02b61de449cf2bf2/narwhals-2.21.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/83/a0/5b0c2f11142ed1dddec842457d3f65eaf71a0080894eb6f018755b319c3a/nbclient-0.10.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/67/f8/bb0a9d5f46819c821dc1f004aa2cc29b1d91453297dbf5ff20470f00f193/nbconvert-7.17.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d3/84/a2d7ced096d0c0c16d663fec2da68a13312e2b621fad19fe324f943c1f55/nebari_jupyterhub_theme-2024.7.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e9/d6/1fd0646b9bbd9efbb0b8ae21b2325fbef515769a5621c03e31d8eb8da587/notebook-7.5.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/2c/ab/fc8290c6a4c722e5514d80f62b2dc4c4df1a68a41d1364e625c35990fcf3/overrides-7.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1d/03/3fc4a529a7710f890a239cc496fc6d50ad4a0995657dccc1d64695adb9f4/pandas-2.3.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/58/85/3290cc84bb35503293ea23d2a0b39a78cf02c560ae1455502b042975c951/panel-1.8.10-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ea/2b/4bc14b04ae260e68c4db7d1e5d2fdda214fb4381236fc8e2d720ed14e2d9/param-2.3.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/5d/8268b644392ee874ee82a635cd0df1773de230bde356c38de28e298392cc/parso-0.8.7-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a1/2a/8c79d6a53169937784604a8ae8d77e45888c41537f7f6f65ed1f407fe66d/pillow-12.2.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/fa/3d/f4f2ba829efb54b6cd2d91349c7463316a9cc55a43fc980447416c88540f/pkginfo-1.12.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/29/1c/1b83ffb56adb76abe46df91439e9a613bba45e4612a4e6a7bbebca0b35bb/plotlydash_tornado_cmd-0.0.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/41/4a/755a692976425af4627ffba016dd0576a045c70066553a27cc4023d44d58/python-keycloak-0.26.1.tar.gz + - pypi: https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bd/2d/e94b2f7bab6773c70efc70a61d66e312e1febccd9e0db6b9e0adf58cbad1/python_jose-3.3.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/78/4126abcbdbd3c559d43e0db7f7b9173fc6befe45d39a2856cc0b8ec2a5a6/python_multipart-0.0.27-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/dd/96da98f892250475bdf2328112d7468abdd4acc7b902b6af23f4ed958ea0/pytz-2026.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/13/5a/f8c0868199bbb231a02616286ce8a4ccb85f5387b9215510297dcfedd214/pyviz_comms-3.0.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/73/78/7d713284dbe022f6440e391bd1f3c48d9185673878034cfb3939cdf333b2/pyzmq-27.1.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a6/24/4d91e05817e92e3a61c8a21e08fd0f390f5301f1c448b137c57c4bc6e543/semver-3.0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1c/78/504fdd027da3b84ff1aecd9f6957e65f35134534ccc6da8628eb71e76d3f/send2trash-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a8/45/a132b9074aa18e799b891b91ad72133c98d8042c70f6240e4c5f9dabee2f/structlog-25.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b5/11/87d6d29fb5d237229d67973a6c9e06e048f01cf4994dee194ab0ea841814/tomlkit-0.14.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7b/e3/d81b065a2d866a33a541ac63a2a4cc5737e03ce2379ac3191c98bb8867e3/trove_classifiers-2026.5.7.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/61/73/d21edf5b204d1467e06500080a50f79d49ef2b997c79123a536d4a17d97c/uc_micro_py-2.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/43/99/3ec6335ded5b88c2f7ed25c56ffd952546f7ed007ffb1e1539dc3b57015a/userpath-1.9.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ee/d5/f3be167a43192062f1409fd6b857a612665d331174293b4ffc73218872e1/uv-0.11.11-py3-none-manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/31/a3/5b1562db76a5a488274b2332a97199b32d0442aca0ed193697fd47786316/uvicorn-0.46.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ca/ff/ded57ac5ff40a09e6e198550bab075d780941e0b0f83cbeabd087c59383a/virtualenv-20.33.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/41/52/e465037f5375f43533d1a80b6923955201596a99142ed524d77b571a1418/wcwidth-0.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a8/a9/d23012099dc88ec69a29c6407b41d89681cb674c2043cd5b467c7e299c08/xyzservices-2026.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/73/28/a44bdece01bca027b079f0e00be3b6bd89a4df180071da59a3dd7381665b/zstandard-0.25.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl packages: - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 sha256: fe51de6107f9edc7aa4f786a70f4a883943bc9d39b3bb7307c04c41410990726 @@ -797,6 +689,11 @@ packages: - pkg:pypi/alembic?source=hash-mapping size: 158380 timestamp: 1741181935 +- pypi: https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl + name: annotated-doc + version: 0.0.4 + sha256: 571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320 + requires_python: '>=3.8' - conda: https://conda.anaconda.org/conda-forge/noarch/annotated-types-0.7.0-pyhd8ed1ab_1.conda sha256: e0ea1ba78fbb64f17062601edda82097fcf815012cf52bb704150a2668110d48 md5: 2934f256a8acfe48f6ebb4fce6cde29c @@ -809,81 +706,43 @@ packages: - pkg:pypi/annotated-types?source=hash-mapping size: 18074 timestamp: 1733247158254 -- conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.9.0-pyh29332c3_0.conda - sha256: b28e0f78bb0c7962630001e63af25a89224ff504e135a02e50d4d80b6155d386 - md5: 9749a2c77a7c40d432ea0927662d7e52 - depends: - - exceptiongroup >=1.0.2 - - idna >=2.8 - - python >=3.9 - - sniffio >=1.1 - - typing_extensions >=4.5 - - python - constrains: - - trio >=0.26.1 - - uvloop >=0.21 - license: MIT - license_family: MIT - purls: - - pkg:pypi/anyio?source=hash-mapping - size: 126346 - timestamp: 1742243108743 -- conda: https://conda.anaconda.org/conda-forge/noarch/appdirs-1.4.4-pyhd8ed1ab_1.conda - sha256: 5b9ef6d338525b332e17c3ed089ca2f53a5d74b7a7b432747d29c6466e39346d - md5: f4e90937bbfc3a4a92539545a37bb448 - depends: - - python >=3.9 - license: MIT - license_family: MIT - purls: - - pkg:pypi/appdirs?source=hash-mapping - size: 14835 - timestamp: 1733754069532 -- conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-23.1.0-pyhd8ed1ab_1.conda - sha256: 7af62339394986bc470a7a231c7f37ad0173ffb41f6bc0e8e31b0be9e3b9d20f - md5: a7ee488b71c30ada51c48468337b85ba - depends: +- pypi: https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl + name: anyio + version: 4.13.0 + sha256: 08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708 + requires_dist: + - exceptiongroup>=1.0.2 ; python_full_version < '3.11' + - idna>=2.8 + - typing-extensions>=4.5 ; python_full_version < '3.13' + - trio>=0.32.0 ; extra == 'trio' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/3b/00/2344469e2084fb287c2e0b57b72910309874c3245463acd6cf5e3db69324/appdirs-1.4.4-py2.py3-none-any.whl + name: appdirs + version: 1.4.4 + sha256: a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128 +- pypi: https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl + name: argon2-cffi + version: 25.1.0 + sha256: fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741 + requires_dist: - argon2-cffi-bindings - - python >=3.9 - - typing-extensions - constrains: - - argon2_cffi ==999 - license: MIT - license_family: MIT - purls: - - pkg:pypi/argon2-cffi?source=hash-mapping - size: 18594 - timestamp: 1733311166338 -- conda: https://conda.anaconda.org/conda-forge/linux-64/argon2-cffi-bindings-21.2.0-py310ha75aee5_5.conda - sha256: 1050f55294476b4d9b36ca3cf22b47f2f23d6e143ad6a177025bc5e5984d5409 - md5: a2da54f3a705d518c95a5b6de8ad8af6 - depends: - - __glibc >=2.17,<3.0.a0 - - cffi >=1.0.1 - - libgcc >=13 - - python >=3.10,<3.11.0a0 - - python_abi 3.10.* *_cp310 - license: MIT - license_family: MIT - purls: - - pkg:pypi/argon2-cffi-bindings?source=hash-mapping - size: 34425 - timestamp: 1725356664523 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/argon2-cffi-bindings-21.2.0-py310ha766c32_5.conda - sha256: ddf10d74c5d42caea8d42657eabb1f7b61ebe348097203044e0ae6d01183250a - md5: 7f6f2689cb20f42767cb421f078a89c2 - depends: - - cffi >=1.0.1 - - libgcc >=13 - - python >=3.10,<3.11.0a0 - - python >=3.10,<3.11.0a0 *_cpython - - python_abi 3.10.* *_cp310 - license: MIT - license_family: MIT - purls: - - pkg:pypi/argon2-cffi-bindings?source=hash-mapping - size: 36082 - timestamp: 1725356813093 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/09/52/94108adfdd6e2ddf58be64f959a0b9c7d4ef2fa71086c38356d22dc501ea/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl + name: argon2-cffi-bindings + version: 25.1.0 + sha256: d3e924cfc503018a714f94a49a149fdc0b644eaead5d1f089330399134fa028a + requires_dist: + - cffi>=1.0.1 ; python_full_version < '3.14' + - cffi>=2.0.0b1 ; python_full_version >= '3.14' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/c1/93/44365f3d75053e53893ec6d733e4a5e3147502663554b4d864587c7828a7/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl + name: argon2-cffi-bindings + version: 25.1.0 + sha256: 1e021e87faa76ae0d413b619fe2b65ab9a037f24c60a1e6cc43457ae20de6dc6 + requires_dist: + - cffi>=1.0.1 ; python_full_version < '3.14' + - cffi>=2.0.0b1 ; python_full_version >= '3.14' + requires_python: '>=3.9' - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.3.0-pyhd8ed1ab_1.conda sha256: c4b0bdb3d5dee50b60db92f99da3e4c524d5240aafc0a5fcc15e45ae2d1a3cd1 md5: 46b53236fdd990271b03c3978d4218a9 @@ -897,32 +756,24 @@ packages: - pkg:pypi/arrow?source=hash-mapping size: 99951 timestamp: 1733584345583 -- conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.0-pyhd8ed1ab_1.conda - sha256: 93b14414b3b3ed91e286e1cbe4e7a60c4e1b1c730b0814d1e452a8ac4b9af593 - md5: 8f587de4bcf981e26228f268df374a9b - depends: - - python >=3.9 - constrains: - - astroid >=2,<4 - license: Apache-2.0 - license_family: Apache - purls: - - pkg:pypi/asttokens?source=hash-mapping - size: 28206 - timestamp: 1733250564754 -- conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.0.5-pyh29332c3_0.conda - sha256: 3b7233041e462d9eeb93ea1dfe7b18aca9c358832517072054bb8761df0c324b - md5: d9d0f99095a9bb7e3641bca8c6ad2ac7 - depends: - - python >=3.9 - - typing_extensions >=4.0.0 - - python - license: MIT - license_family: MIT - purls: - - pkg:pypi/async-lru?source=hash-mapping - size: 17335 - timestamp: 1742153708859 +- pypi: https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl + name: asttokens + version: 3.0.1 + sha256: 15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a + requires_dist: + - astroid>=2,<5 ; extra == 'astroid' + - astroid>=2,<5 ; extra == 'test' + - pytest<9.0 ; extra == 'test' + - pytest-cov ; extra == 'test' + - pytest-xdist ; extra == 'test' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/e5/e2/c2e3abf398f80732e58b03be77bde9022550d221dd8781bf586bd4d97cc1/async_lru-2.3.0-py3-none-any.whl + name: async-lru + version: 2.3.0 + sha256: eea27b01841909316f2cc739807acea1c623df2be8c5cfad7583286397bb8315 + requires_dist: + - typing-extensions>=4.0.0 ; python_full_version < '3.11' + requires_python: '>=3.10' - conda: https://conda.anaconda.org/conda-forge/noarch/async-timeout-5.0.1-pyhd8ed1ab_1.conda sha256: 33d12250c870e06c9a313c6663cfbf1c50380b73dfbbb6006688c3134b29b45a md5: 5d842988b11a8c3ab57fb70840c83d24 @@ -956,77 +807,69 @@ packages: - pkg:pypi/attrs?source=compressed-mapping size: 57181 timestamp: 1741918625732 -- conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.17.0-pyhd8ed1ab_0.conda - sha256: 1c656a35800b7f57f7371605bc6507c8d3ad60fbaaec65876fce7f73df1fc8ac - md5: 0a01c169f0ab0f91b26e77a3301fbfe4 - depends: - - python >=3.9 - - pytz >=2015.7 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/babel?source=compressed-mapping - size: 6938256 - timestamp: 1738490268466 -- conda: https://conda.anaconda.org/conda-forge/noarch/backports-1.0-pyhd8ed1ab_5.conda - sha256: e1c3dc8b5aa6e12145423fed262b4754d70fec601339896b9ccf483178f690a6 - md5: 767d508c1a67e02ae8f50e44cacfadb2 - depends: - - python >=3.9 - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 7069 - timestamp: 1733218168786 -- conda: https://conda.anaconda.org/conda-forge/noarch/backports.tarfile-1.2.0-pyhd8ed1ab_1.conda - sha256: a0f41db6d7580cec3c850e5d1b82cb03197dd49a3179b1cee59c62cd2c761b36 - md5: df837d654933488220b454c6a3b0fad6 - depends: - - backports - - python >=3.9 - license: MIT - license_family: MIT - purls: - - pkg:pypi/backports-tarfile?source=hash-mapping - size: 32786 - timestamp: 1733325872620 -- conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.13.3-pyha770c72_0.conda - sha256: 4ce42860292a57867cfc81a5d261fb9886fc709a34eca52164cc8bbf6d03de9f - md5: 373374a3ed20141090504031dc7b693e - depends: - - python >=3.9 - - soupsieve >=1.2 - - typing-extensions - license: MIT - license_family: MIT - purls: - - pkg:pypi/beautifulsoup4?source=compressed-mapping - size: 145482 - timestamp: 1738740460562 -- conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.2.0-pyh29332c3_4.conda - sha256: a05971bb80cca50ce9977aad3f7fc053e54ea7d5321523efc7b9a6e12901d3cd - md5: f0b4c8e370446ef89797608d60a564b3 - depends: - - python >=3.9 +- pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl + name: babel + version: 2.18.0 + sha256: e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35 + requires_dist: + - pytz>=2015.7 ; python_full_version < '3.9' + - tzdata ; sys_platform == 'win32' and extra == 'dev' + - backports-zoneinfo ; python_full_version < '3.9' and extra == 'dev' + - freezegun~=1.0 ; extra == 'dev' + - jinja2>=3.0 ; extra == 'dev' + - pytest-cov ; extra == 'dev' + - pytest>=6.0 ; extra == 'dev' + - pytz ; extra == 'dev' + - setuptools ; extra == 'dev' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl + name: backports-tarfile + version: 1.2.0 + sha256: 77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34 + requires_dist: + - sphinx>=3.5 ; extra == 'docs' + - jaraco-packaging>=9.3 ; extra == 'docs' + - rst-linker>=1.9 ; extra == 'docs' + - furo ; extra == 'docs' + - sphinx-lint ; extra == 'docs' + - pytest>=6,!=8.1.* ; extra == 'testing' + - pytest-checkdocs>=2.4 ; extra == 'testing' + - pytest-cov ; extra == 'testing' + - pytest-enabler>=2.2 ; extra == 'testing' + - jaraco-test ; extra == 'testing' + - pytest!=8.0.* ; extra == 'testing' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/90/a7/a521ea420aded33ca9821b3ef98e5bbdbd78d5d41711684cfce1a5d127a8/backports_zstd-1.4.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + name: backports-zstd + version: 1.4.0 + sha256: e5d269184a1c310a69b2c3a8dcb6c87de90fae4e9252852918a740dbca780648 + requires_python: '>=3.10,<3.14' +- pypi: https://files.pythonhosted.org/packages/ea/cf/7fc0d8c3b5bae488ddab183a203474d9066fe77b0b563b3a345fc141ac50/backports_zstd-1.4.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: backports-zstd + version: 1.4.0 + sha256: 8b948ffb0697d1cdeed49ac20595532cdbe4968468426418d399c66e2a4c9dd3 + requires_python: '>=3.10,<3.14' +- pypi: https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl + name: beautifulsoup4 + version: 4.14.3 + sha256: 0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb + requires_dist: + - soupsieve>=1.6.1 + - typing-extensions>=4.0.0 + - cchardet ; extra == 'cchardet' + - chardet ; extra == 'chardet' + - charset-normalizer ; extra == 'charset-normalizer' + - html5lib ; extra == 'html5lib' + - lxml ; extra == 'lxml' + requires_python: '>=3.7.0' +- pypi: https://files.pythonhosted.org/packages/cd/3a/577b549de0cc09d95f11087ee63c739bba856cd3952697eec4c4bb91350a/bleach-6.3.0-py3-none-any.whl + name: bleach + version: 6.3.0 + sha256: fe10ec77c93ddf3d13a73b035abaac7a9f5e436513864ccdad516693213c65d6 + requires_dist: - webencodings - - python - constrains: - - tinycss >=1.1.0,<1.5 - license: Apache-2.0 AND MIT - purls: - - pkg:pypi/bleach?source=hash-mapping - size: 141405 - timestamp: 1737382993425 -- conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.2.0-h82add2a_4.conda - sha256: 0aba699344275b3972bd751f9403316edea2ceb942db12f9f493b63c74774a46 - md5: a30e9406c873940383555af4c873220d - depends: - - bleach ==6.2.0 pyh29332c3_4 - - tinycss2 - license: Apache-2.0 AND MIT - purls: [] - size: 4213 - timestamp: 1737382993425 + - tinycss2>=1.1.0,<1.5 ; extra == 'css' + requires_python: '>=3.10' - conda: https://conda.anaconda.org/conda-forge/noarch/blinker-1.9.0-pyhff2d567_0.conda sha256: f7efd22b5c15b400ed84a996d777b6327e5c402e79e3c534a7e086236f1eb2dc md5: 42834439227a4551b939beeeb8a4b085 @@ -1038,38 +881,33 @@ packages: - pkg:pypi/blinker?source=hash-mapping size: 13934 timestamp: 1731096548765 -- conda: https://conda.anaconda.org/conda-forge/noarch/bokeh-3.6.3-pyhd8ed1ab_0.conda - sha256: 6cc6841b1660cd3246890d4f601baf51367526afe6256dfd8a8d9a8f7db651fe - md5: 606498329a91bd9d5c0439fb2815816f - depends: - - contourpy >=1.2 - - jinja2 >=2.9 - - numpy >=1.16 - - packaging >=16.8 - - pandas >=1.2 - - pillow >=7.1.0 - - python >=3.10 - - pyyaml >=3.10 - - tornado >=6.2 - - xyzservices >=2021.09.1 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/bokeh?source=hash-mapping - size: 4524790 - timestamp: 1738843545439 -- conda: https://conda.anaconda.org/conda-forge/noarch/bokeh-root-cmd-0.1.2-pyhd8ed1ab_1.conda - sha256: 4a37d16f65e0b12cfec91e34661c55a58a43b2b61dce7e6cd5c46b3f1611fd63 - md5: 3b94739e0849ca6a9e97401d87222188 - depends: - - click >=7.0 - - python >=3.9,<4.0.0 - license: Apache-2.0 - license_family: APACHE - purls: - - pkg:pypi/bokeh-root-cmd?source=hash-mapping - size: 15566 - timestamp: 1735207724198 +- pypi: https://files.pythonhosted.org/packages/47/0b/bdf449df87be3f07b23091ceafee8c3ef569cf6d2fb7edec6e3b12b3faa4/bokeh-3.9.0-py3-none-any.whl + name: bokeh + version: 3.9.0 + sha256: b252bfb16a505f0e0c57d532d0df308ae1667235bafc622aa9441fe9e7c5ce4a + requires_dist: + - jinja2>=2.9 + - contourpy>=1.2 + - narwhals>=1.13 + - numpy>=1.16 + - packaging>=16.8 + - pillow>=7.1.0 + - pyyaml>=3.10 + - tornado>=6.2 ; sys_platform != 'emscripten' + - xyzservices>=2021.9.1 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/92/60/aec8c2f63f49a5e3064362d31d37c0ab2a43b0dc3e093e11ca0034f4cf49/bokeh_root_cmd-0.1.2-py3-none-any.whl + name: bokeh-root-cmd + version: 0.1.2 + sha256: 823030110f322c8a4abb99a9d002cdcb09f30175d4dd0f27a90ce92731c03510 + requires_dist: + - click>=7.0 + requires_python: '>=3.6' +- pypi: https://files.pythonhosted.org/packages/45/7f/0e961cf3908bc4c1c3e027de2794f867c6c89fb4916fc7dba295a0e80a2d/boltons-25.0.0-py3-none-any.whl + name: boltons + version: 25.0.0 + sha256: dc9fb38bf28985715497d1b54d00b62ea866eca3938938ea9043e254a3a6ca62 + requires_python: '>=3.7' - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py310hf71b8c6_2.conda sha256: 14f1e89d3888d560a553f40ac5ba83e4435a107552fa5b2b2029a7472554c1ef md5: bf502c169c71e3c6ac0d6175addfacc2 @@ -1104,6 +942,21 @@ packages: - pkg:pypi/brotli?source=hash-mapping size: 356792 timestamp: 1725267937299 +- pypi: https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl + name: build + version: 1.5.0 + sha256: 13f3eecb844759ab66efec90ca17639bbf14dc06cb2fdf37a9010322d9c50a6f + requires_dist: + - packaging>=24.0 + - pyproject-hooks + - colorama ; os_name == 'nt' + - importlib-metadata>=4.6 ; python_full_version < '3.10.2' + - tomli>=1.1.0 ; python_full_version < '3.11' + - keyring ; extra == 'keyring' + - uv>=0.1.18 ; extra == 'uv' + - virtualenv>=20.17 ; python_full_version >= '3.10' and python_full_version < '3.14' and extra == 'virtualenv' + - virtualenv>=20.31 ; python_full_version >= '3.14' and extra == 'virtualenv' + requires_python: '>=3.10' - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda sha256: 5ced96500d945fb286c9c838e54fa759aa04a7129c59800f0846b4335cee770d md5: 62ee74e96c5ebb0af99386de58cf9553 @@ -1160,31 +1013,29 @@ packages: purls: [] size: 158290 timestamp: 1738299057652 -- conda: https://conda.anaconda.org/conda-forge/noarch/cachecontrol-0.14.2-pyha770c72_0.conda - sha256: 5684d23509525b65dd019a70bbb73c987a5d64177c0ce3def3dfdb175687ea27 - md5: df6a1180171318e6a58c206c38ff66fd - depends: - - msgpack-python >=0.5.2,<2.0.0 - - python >=3.9 - - requests >=2.16.0 - license: Apache-2.0 - license_family: Apache - purls: - - pkg:pypi/cachecontrol?source=hash-mapping - size: 23381 - timestamp: 1736337985490 -- conda: https://conda.anaconda.org/conda-forge/noarch/cachecontrol-with-filecache-0.14.2-pyhd8ed1ab_0.conda - sha256: cee46674041043c046232c6334b25487caa5c3d57c8b78adec0265afade4bda3 - md5: 193d7362ba6d1b551ffe7b1da103f47f - depends: - - cachecontrol 0.14.2 pyha770c72_0 - - filelock >=3.8.0 - - python >=3.9 - license: Apache-2.0 - license_family: Apache - purls: [] - size: 6719 - timestamp: 1736337996330 +- pypi: https://files.pythonhosted.org/packages/ef/79/c45f2d53efe6ada1110cf6f9fca095e4ff47a0454444aefdde6ac4789179/cachecontrol-0.14.4-py3-none-any.whl + name: cachecontrol + version: 0.14.4 + sha256: b7ac014ff72ee199b5f8af1de29d60239954f223e948196fa3d84adaffc71d2b + requires_dist: + - requests>=2.16.0 + - msgpack>=0.5.2,<2.0.0 + - cachecontrol[filecache,redis] ; extra == 'dev' + - cherrypy ; extra == 'dev' + - cheroot>=11.1.2 ; extra == 'dev' + - codespell ; extra == 'dev' + - furo ; extra == 'dev' + - mypy ; extra == 'dev' + - pytest ; extra == 'dev' + - pytest-cov ; extra == 'dev' + - ruff ; extra == 'dev' + - sphinx ; extra == 'dev' + - sphinx-copybutton ; extra == 'dev' + - types-redis ; extra == 'dev' + - types-requests ; extra == 'dev' + - filelock>=3.8.0 ; extra == 'filecache' + - redis>=2.10.5 ; extra == 'redis' + requires_python: '>=3.10' - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 noarch: python sha256: 561e6660f26c35d137ee150187d89767c988413c978e1b712d53f27ddf70ea17 @@ -1218,17 +1069,6 @@ packages: - pkg:pypi/cachetools?source=compressed-mapping size: 15220 timestamp: 1740094145914 -- conda: https://conda.anaconda.org/conda-forge/noarch/cachy-0.3.0-pyhd8ed1ab_2.conda - sha256: 2560a98e3dc0ff4ff408a199d05922ae10fab2629417c4c4309e4226267cef8c - md5: 1efe226b868cf59b8330356c37c8186e - depends: - - python >=3.9 - license: MIT - license_family: MIT - purls: - - pkg:pypi/cachy?source=hash-mapping - size: 22762 - timestamp: 1734685195915 - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.1.31-pyhd8ed1ab_0.conda sha256: 42a78446da06a2568cb13e69be3355169fbd0ea424b00fc80b7d840f5baaacf3 md5: c207fa5ac7ea99b149344385a9c0880d @@ -1293,122 +1133,111 @@ packages: - pkg:pypi/charset-normalizer?source=hash-mapping size: 47438 timestamp: 1735929811779 -- conda: https://conda.anaconda.org/conda-forge/noarch/click-8.1.8-pyh707e725_0.conda - sha256: c920d23cd1fcf565031c679adb62d848af60d6fbb0edc2d50ba475cea4f0d8ab - md5: f22f4d4970e09d68a10b922cbb0408d3 - depends: - - __unix - - python >=3.9 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/click?source=hash-mapping - size: 84705 - timestamp: 1734858922844 -- conda: https://conda.anaconda.org/conda-forge/noarch/click-default-group-1.2.4-pyhd8ed1ab_1.conda - sha256: cb7279eecddbd35ea78fd0e189a9a7db8b84c2c0e3b1271cf26251615f75dc4d - md5: 7cd83dd6831b61ad9624a694e4afd7dc - depends: +- pypi: https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl + name: click + version: 8.3.3 + sha256: a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613 + requires_dist: + - colorama ; sys_platform == 'win32' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/2c/1a/aff8bb287a4b1400f69e09a53bd65de96aa5cee5691925b38731c67fc695/click_default_group-1.2.4-py2.py3-none-any.whl + name: click-default-group + version: 1.2.4 + sha256: 9b60486923720e7fc61731bdb32b617039aba820e22e1c88766b1125592eaa5f + requires_dist: - click - - python >=3.9 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/click-default-group?source=hash-mapping - size: 10124 - timestamp: 1734029586298 -- conda: https://conda.anaconda.org/conda-forge/noarch/clikit-0.6.2-pyhd8ed1ab_3.conda - sha256: da000653be96a15b9aad5c59f655dbd4a60cb66fc0137e1018db9de76671bb08 - md5: 37e178bf9356122c35005a62d850e5d9 - depends: - - pastel >=0.2.0,<0.3.0 - - pylev >=1.3,<2.0 - - python >=3.9 - license: MIT - license_family: MIT - purls: - - pkg:pypi/clikit?source=hash-mapping - size: 64319 - timestamp: 1734603385360 -- conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - sha256: ab29d57dc70786c1269633ba3dff20288b81664d3ff8d21af995742e2bb03287 - md5: 962b9857ee8e7018c22f2776ffa0b2d7 - depends: - - python >=3.9 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/colorama?source=hash-mapping - size: 27011 - timestamp: 1733218222191 -- conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.2-pyhd8ed1ab_1.conda - sha256: 7e87ef7c91574d9fac19faedaaee328a70f718c9b4ddadfdc0ba9ac021bd64af - md5: 74673132601ec2b7fc592755605f4c1b - depends: - - python >=3.9 - - traitlets >=5.3 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/comm?source=hash-mapping - size: 12103 - timestamp: 1733503053903 -- conda: https://conda.anaconda.org/conda-forge/noarch/conda-lock-2.5.7-pyhd8ed1ab_1.conda - sha256: 905618b595d7a067fe37a282e3b84a4ed46542c1b497c76cef7b0f33f9335cb7 - md5: 518d59879a7ba4f3972109e8666860b2 - depends: - - cachecontrol-with-filecache >=0.12.9 - - cachy >=0.3.0 - - click >=8.0 + - pytest ; extra == 'test' + requires_python: '>=2.7' +- pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl + name: comm + version: 0.2.3 + sha256: c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417 + requires_dist: + - pytest ; extra == 'test' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/31/28/237390aa8425404fbe15dbca65e724e1b3ac28be9215568c25d455f53111/conda_lock-4.0.0-py3-none-any.whl + name: conda-lock + version: 4.0.0 + sha256: 979aacb4f67eb375f5d84ffbc7afc93c881ab9a513558c7461ed955fe243a3f6 + requires_dist: + - boltons>=23.0.0 + - build>=1.2.1,<2.0.0 + - cachecontrol[filecache]>=0.14.0,<0.15.0 + - charset-normalizer - click-default-group - - clikit >=0.6.2 - - crashtest >=0.3.0 - - ensureconda >=1.3 - - gitpython >=3.1.30 - - html5lib >=1.0 + - click>=8.0 + - crashtest>=0.4.1,<0.5.0 + - dulwich>=0.22.6,<0.25 + - ensureconda>=1.4.7 + - fastjsonschema>=2.18.0,<3.0.0 + - filelock + - gitpython>=3.1.30 + - importlib-metadata>=4.4 ; python_full_version < '3.10' + - installer>=0.7.0,<0.8.0 - jinja2 - - keyring >=21.2.0 - - packaging >=20.4 - - pkginfo >=1.4 - - pydantic >=1.10 - - python >=3.9 - - pyyaml >=5.1 - - requests >=2.18 - - ruamel.yaml + - keyring>=25.1.0,<26.0.0 + - packaging>=24.0 + - pkginfo>=1.12,<2.0 + - platformdirs>=3.10.0,<5.0.0 + - pydantic>=2 + - pyproject-hooks>=1.0.0,<2.0.0 + - pyyaml>=5.1 + - requests-toolbelt>=1.0.0,<2.0.0 + - requests>=2.26,<3.0 + - ruamel-yaml + - semver>=3,<4 - setuptools - - tomli - - tomlkit >=0.7.0 - - toolz >=0.12.0,<1.0.0 - - typing_extensions - - urllib3 >=1.26.5,<2.0 - - virtualenv >=20.0.26 - license: MIT - license_family: MIT - purls: - - pkg:pypi/conda-lock?source=hash-mapping - size: 884197 - timestamp: 1734673596560 -- conda: https://conda.anaconda.org/conda-forge/noarch/conda-project-0.4.2-pyhd8ed1ab_1.conda - sha256: 1f1b376d3618672e50537fbeba345361cb0fd92a2b0396f035711e0555144275 - md5: 15134d218d9261332268c0038b7d3ab4 - depends: - - conda-lock >=2.5.6 + - shellingham>=1.5,<2.0 + - tomli>=2.0.1,<3.0.0 ; python_full_version < '3.11' + - tomlkit>=0.11.4,<1.0.0 + - trove-classifiers>=2022.5.19 + - typing-extensions>=4.6.1 + - virtualenv>=20.26.6,<21.0.0 + - xattr>=1.0.0,<2.0.0 ; sys_platform == 'darwin' + - zstandard>=0.15 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/87/39/0bd20aa98ae43756b0bb5d8b055c19de842b422fe588eaa16f11dce1f399/conda_package_streaming-0.12.0-py3-none-any.whl + name: conda-package-streaming + version: 0.12.0 + sha256: 45ae0ac0e198188703e845a42ac21292b73cd7077df358ea882d25923ab26f96 + requires_dist: + - requests + - zstandard>=0.15 + - furo ; extra == 'docs' + - sphinx ; extra == 'docs' + - myst-parser ; extra == 'docs' + - mdit-py-plugins>=0.3.0 ; extra == 'docs' + - pytest>=7 ; extra == 'test' + - pytest-cov ; extra == 'test' + - pytest-mock ; extra == 'test' + - boto3 ; extra == 'test' + - boto3-stubs[essential] ; extra == 'test' + - bottle ; extra == 'test' + - conda ; extra == 'test' + - conda-package-handling>=2 ; extra == 'test' + - responses ; extra == 'test' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/f3/3e/a548d42e64c4928ae60b03339c955ad115fdc5d049cdaf07112b789d1862/conda_project-0.4.2-py3-none-any.whl + name: conda-project + version: 0.4.2 + sha256: a20ae5c8fed8a2a0245846929021cfe856212b176618bf8ac63bfe67ff127ce8 + requires_dist: + - conda-lock>=2.5.6 - fsspec + - libarchive-c - lockfile - pexpect - pydantic - - python >=3.9,<3.13 - python-dotenv - - python-libarchive-c - - ruamel.yaml + - ruamel-yaml - setuptools - shellingham - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/conda-project?source=hash-mapping - size: 29910 - timestamp: 1736216919707 + - conda-sphinx-theme>=0.1.1 ; extra == 'docs' + - myst-parser>=0.18.0 ; extra == 'docs' + - sphinx-autobuild>=2021.3.14 ; extra == 'docs' + - sphinx-autodoc-typehints>=1.19.2 ; extra == 'docs' + - sphinx>=5.1.1 ; extra == 'docs' + requires_python: '>=3.8' - conda: https://conda.anaconda.org/conda-forge/linux-64/configurable-http-proxy-4.6.3-hbf95b10_0.conda sha256: f3f6b2ceeec8134289b530be884340ad978263c618d6fec84647c3ee482ebf80 md5: 7ede2481c094a10d4958e255ccaf3268 @@ -1429,49 +1258,61 @@ packages: purls: [] size: 1292600 timestamp: 1736705738534 -- conda: https://conda.anaconda.org/conda-forge/linux-64/contourpy-1.3.1-py310h3788b33_0.conda - sha256: 1b18ebb72fb20b9ece47c582c6112b1d4f0f7deebaa056eada99e1f994e8a81f - md5: f993b13665fc2bb262b30217c815d137 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - libstdcxx >=13 - - numpy >=1.23 - - python >=3.10,<3.11.0a0 - - python_abi 3.10.* *_cp310 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/contourpy?source=hash-mapping - size: 260973 - timestamp: 1731428528301 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/contourpy-1.3.1-py310hf54e67a_0.conda - sha256: ea93636b22b04e49080b7a8d44a0c943390a345a43d3b6e3fb9bf78a989383b9 - md5: 4dd4efc74373cb53f9c1191f768a9b45 - depends: - - libgcc >=13 - - libstdcxx >=13 - - numpy >=1.23 - - python >=3.10,<3.11.0a0 - - python >=3.10,<3.11.0a0 *_cpython - - python_abi 3.10.* *_cp310 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/contourpy?source=hash-mapping - size: 269477 - timestamp: 1731428554876 -- conda: https://conda.anaconda.org/conda-forge/noarch/crashtest-0.4.1-pyhd8ed1ab_1.conda - sha256: af1622b15f8c7411d9c14b8adf970cec16fec8a28b98069fdf42b1cd2259ccc9 - md5: e036e2f76d9c9aebc12510ed23352b6c - depends: - - python >=3.9,<4.0 - license: MIT - license_family: MIT - purls: - - pkg:pypi/crashtest?source=hash-mapping - size: 11619 - timestamp: 1733564888371 +- pypi: https://files.pythonhosted.org/packages/32/5c/1ee32d1c7956923202f00cf8d2a14a62ed7517bdc0ee1e55301227fc273c/contourpy-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: contourpy + version: 1.3.2 + sha256: ad687a04bc802cbe8b9c399c07162a3c35e227e2daccf1668eb1f278cb698631 + requires_dist: + - numpy>=1.23 + - furo ; extra == 'docs' + - sphinx>=7.2 ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - bokeh ; extra == 'bokeh' + - selenium ; extra == 'bokeh' + - contourpy[bokeh,docs] ; extra == 'mypy' + - bokeh ; extra == 'mypy' + - docutils-stubs ; extra == 'mypy' + - mypy==1.15.0 ; extra == 'mypy' + - types-pillow ; extra == 'mypy' + - contourpy[test-no-images] ; extra == 'test' + - matplotlib ; extra == 'test' + - pillow ; extra == 'test' + - pytest ; extra == 'test-no-images' + - pytest-cov ; extra == 'test-no-images' + - pytest-rerunfailures ; extra == 'test-no-images' + - pytest-xdist ; extra == 'test-no-images' + - wurlitzer ; extra == 'test-no-images' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/c1/bd/20c6726b1b7f81a8bee5271bed5c165f0a8e1f572578a9d27e2ccb763cb2/contourpy-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl + name: contourpy + version: 1.3.2 + sha256: 9be002b31c558d1ddf1b9b415b162c603405414bacd6932d031c5b5a8b757f0d + requires_dist: + - numpy>=1.23 + - furo ; extra == 'docs' + - sphinx>=7.2 ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - bokeh ; extra == 'bokeh' + - selenium ; extra == 'bokeh' + - contourpy[bokeh,docs] ; extra == 'mypy' + - bokeh ; extra == 'mypy' + - docutils-stubs ; extra == 'mypy' + - mypy==1.15.0 ; extra == 'mypy' + - types-pillow ; extra == 'mypy' + - contourpy[test-no-images] ; extra == 'test' + - matplotlib ; extra == 'test' + - pillow ; extra == 'test' + - pytest ; extra == 'test-no-images' + - pytest-cov ; extra == 'test-no-images' + - pytest-rerunfailures ; extra == 'test-no-images' + - pytest-xdist ; extra == 'test-no-images' + - wurlitzer ; extra == 'test-no-images' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/b0/5c/3ba7d12e7a79566f97b8f954400926d7b6eb33bcdccc1315a857f200f1f1/crashtest-0.4.1-py3-none-any.whl + name: crashtest + version: 0.4.1 + sha256: 8d23eac5fa660409f57472e3851dab7ac18aba459a8d19cbbba86d3d5aecd2a5 + requires_python: '>=3.7,<4.0' - conda: https://conda.anaconda.org/conda-forge/linux-64/cryptography-44.0.2-py310h6c63255_0.conda sha256: 9794c829047f951e8f23d70e6521e2fcae2e43e0dd1589dab69c791f6995bfc7 md5: 24e325f1ed329640c3e4fc7add288363 @@ -1541,114 +1382,61 @@ packages: purls: [] size: 195461 timestamp: 1767821684720 -- conda: https://conda.anaconda.org/conda-forge/linux-64/dbus-1.13.6-h5008d03_3.tar.bz2 - sha256: 8f5f995699a2d9dbdd62c61385bfeeb57c82a681a7c8c5313c395aa0ccab68a5 - md5: ecfff944ba3960ecb334b9a2663d708d - depends: - - expat >=2.4.2,<3.0a0 - - libgcc-ng >=9.4.0 - - libglib >=2.70.2,<3.0a0 - license: GPL-2.0-or-later - license_family: GPL - purls: [] - size: 618596 - timestamp: 1640112124844 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/dbus-1.13.6-h12b9eeb_3.tar.bz2 - sha256: 5fe76bdf27a142cfb9da0fb3197c562e528d2622b573765bee5c9904cf5e6b6b - md5: f3d63805602166bac09386741e00935e - depends: - - expat >=2.4.2,<3.0a0 - - libgcc-ng >=9.4.0 - - libglib >=2.70.2,<3.0a0 - license: GPL-2.0-or-later - license_family: GPL - purls: [] - size: 672759 - timestamp: 1640113663539 -- conda: https://conda.anaconda.org/conda-forge/linux-64/debugpy-1.8.13-py310hf71b8c6_0.conda - sha256: f02fb1862980595a310551f5cd35ddffa1f224e7d9c5aab6c3b9e37922a96e34 - md5: dc30b46d5b3ddccd3b3ac1b0bee17026 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - libstdcxx >=13 - - python >=3.10,<3.11.0a0 - - python_abi 3.10.* *_cp310 - license: MIT - license_family: MIT - purls: - - pkg:pypi/debugpy?source=hash-mapping - size: 2140729 - timestamp: 1741148580365 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/debugpy-1.8.13-py310he30c3ed_0.conda - sha256: 9cfddba2fda9d634ef46944ee4c793fb6350493a1c91a2cee26e307d37165c77 - md5: 854be3890e693317a8364b9aa87cb554 - depends: - - libgcc >=13 - - libstdcxx >=13 - - python >=3.10,<3.11.0a0 - - python >=3.10,<3.11.0a0 *_cpython - - python_abi 3.10.* *_cp310 - license: MIT - license_family: MIT - purls: - - pkg:pypi/debugpy?source=hash-mapping - size: 2096921 - timestamp: 1741148605065 -- conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda - sha256: c17c6b9937c08ad63cb20a26f403a3234088e57d4455600974a0ce865cb14017 - md5: 9ce473d1d1be1cc3810856a48b3fab32 - depends: - - python >=3.9 - license: BSD-2-Clause - license_family: BSD - purls: - - pkg:pypi/decorator?source=compressed-mapping - size: 14129 - timestamp: 1740385067843 -- conda: https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 - sha256: 9717a059677553562a8f38ff07f3b9f61727bd614f505658b0a5ecbcf8df89be - md5: 961b3a227b437d82ad7054484cfa71b2 - depends: - - python >=3.6 - license: PSF-2.0 - license_family: PSF - purls: - - pkg:pypi/defusedxml?source=hash-mapping - size: 24062 - timestamp: 1615232388757 -- conda: https://conda.anaconda.org/conda-forge/noarch/distlib-0.3.9-pyhd8ed1ab_1.conda - sha256: 0e160c21776bd881b79ce70053e59736f51036784fa43a50da10a04f0c1b9c45 - md5: 8d88f4a2242e6b96f9ecff9a6a05b2f1 - depends: - - python >=3.9 - license: Apache-2.0 - license_family: APACHE - purls: - - pkg:pypi/distlib?source=hash-mapping - size: 274151 - timestamp: 1733238487461 -- conda: https://conda.anaconda.org/conda-forge/noarch/dnspython-2.7.0-pyhff2d567_1.conda - sha256: 3ec40ccf63f2450c5e6c7dd579e42fc2e97caf0d8cd4ba24aa434e6fc264eda0 - md5: 5fbd60d61d21b4bd2f9d7a48fe100418 - depends: - - python >=3.9,<4.0.0 - - sniffio - constrains: - - aioquic >=1.0.0 - - wmi >=1.5.1 - - httpx >=0.26.0 - - trio >=0.23 - - cryptography >=43 - - httpcore >=1.0.0 - - idna >=3.7 - - h2 >=4.1.0 - license: ISC - license_family: OTHER - purls: - - pkg:pypi/dnspython?source=hash-mapping - size: 172172 - timestamp: 1733256829961 +- pypi: https://files.pythonhosted.org/packages/e0/c3/7f67dea8ccf8fdcb9c99033bbe3e90b9e7395415843accb81428c441be2d/debugpy-1.8.20-py2.py3-none-any.whl + name: debugpy + version: 1.8.20 + sha256: 5be9bed9ae3be00665a06acaa48f8329d2b9632f15fd09f6a9a8c8d9907e54d7 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl + name: decorator + version: 5.2.1 + sha256: d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl + name: defusedxml + version: 0.7.1 + sha256: a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61 + requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*' +- pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl + name: distlib + version: 0.4.0 + sha256: 9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 +- pypi: https://files.pythonhosted.org/packages/5e/a6/c13168490a926f84447e9035fb5d10504d7c4d81a68162a900c59b1fc0fe/dulwich-0.24.1-cp310-cp310-manylinux_2_28_aarch64.whl + name: dulwich + version: 0.24.1 + sha256: d16507ca6d6c2d29d7d942da4cc50fa589d58ab066030992dfa3932de6695062 + requires_dist: + - urllib3>=1.25 + - typing-extensions>=4.0 ; python_full_version < '3.11' + - fastimport ; extra == 'fastimport' + - urllib3>=1.24.1 ; extra == 'https' + - gpg ; extra == 'pgp' + - paramiko ; extra == 'paramiko' + - rich ; extra == 'colordiff' + - ruff==0.12.4 ; extra == 'dev' + - mypy==1.17.0 ; extra == 'dev' + - dissolve>=0.1.1 ; extra == 'dev' + - merge3 ; extra == 'merge' + - atheris ; extra == 'fuzzing' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/b9/f9/c248185bf982f2efbc02f1de5783b0e4aae94975b07d293e2aae1901c4b1/dulwich-0.24.1-cp310-cp310-manylinux_2_28_x86_64.whl + name: dulwich + version: 0.24.1 + sha256: e893b800c72499e21d0160169bac574292626193532c336ffce7617fe02d97db + requires_dist: + - urllib3>=1.25 + - typing-extensions>=4.0 ; python_full_version < '3.11' + - fastimport ; extra == 'fastimport' + - urllib3>=1.24.1 ; extra == 'https' + - gpg ; extra == 'pgp' + - paramiko ; extra == 'paramiko' + - rich ; extra == 'colordiff' + - ruff==0.12.4 ; extra == 'dev' + - mypy==1.17.0 ; extra == 'dev' + - dissolve>=0.1.1 ; extra == 'dev' + - merge3 ; extra == 'merge' + - atheris ; extra == 'fuzzing' + requires_python: '>=3.9' - conda: https://conda.anaconda.org/conda-forge/noarch/durationpy-0.9-pyhd8ed1ab_1.conda sha256: 045055a25f47473759eea9c710cc4b6d27a4f4db8e34d96771a830435d2e9a27 md5: f7e6b04ecc949a19de49cbcd3f14addf @@ -1669,54 +1457,31 @@ packages: - gmpy ; extra == 'gmpy' - gmpy2 ; extra == 'gmpy2' requires_python: '>=2.6,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*' -- conda: https://conda.anaconda.org/conda-forge/noarch/editables-0.5-pyhd8ed1ab_1.conda - sha256: 8d4f908e670be360617d418c328213bc46e7100154c3742db085148141712f60 - md5: 2cf824fe702d88e641eec9f9f653e170 - depends: - - python >=3.9 - license: MIT - license_family: MIT - purls: - - pkg:pypi/editables?source=hash-mapping - size: 10828 - timestamp: 1733208220327 -- conda: https://conda.anaconda.org/conda-forge/noarch/email-validator-2.2.0-pyhd8ed1ab_1.conda - sha256: b91a19eb78edfc2dbb36de9a67f74ee2416f1b5273dd7327abe53f2dbf864736 - md5: da16dd3b0b71339060cd44cb7110ddf9 - depends: - - dnspython >=2.0.0 - - idna >=2.0.0 - - python >=3.9 - license: Unlicense - purls: - - pkg:pypi/email-validator?source=hash-mapping - size: 44401 - timestamp: 1733300827551 -- conda: https://conda.anaconda.org/conda-forge/noarch/email_validator-2.2.0-hd8ed1ab_1.conda - sha256: e0d0fdf587aa0ed0ff08b2bce3ab355f46687b87b0775bfba01cc80a859ee6a2 - md5: 0794f8807ff2c6f020422cacb1bd7bfa - depends: - - email-validator >=2.2.0,<2.2.1.0a0 - license: Unlicense - purls: [] - size: 6552 - timestamp: 1733300828176 -- conda: https://conda.anaconda.org/conda-forge/noarch/ensureconda-1.4.4-pyhd8ed1ab_1.conda - sha256: 4efc864d9245a30f15bbc6eb12d06a5cf7a11d91d3e2c84630df1ce83f8b9878 - md5: a18423d4b24e6480165a38f102ca8b49 - depends: +- pypi: https://files.pythonhosted.org/packages/f5/64/82570019b199a85b1d9559e75f448272fc6a79201d8692a1357f3f803837/ensureconda-1.6.0-py3-none-any.whl + name: ensureconda + version: 1.6.0 + sha256: df518b64b08640a1e5e37b1c80d90810a8f5ad0a1b9938aaa740653bb66ef538 + requires_dist: - appdirs - - click >=5.1 + - click>=5.1 + - conda-package-streaming - filelock - packaging - - python >=3.9 - - requests >=2 - license: MIT - license_family: MIT - purls: - - pkg:pypi/ensureconda?source=hash-mapping - size: 14405 - timestamp: 1734637240894 + - requests>=2 + - black ; extra == 'dev' + - build ; extra == 'dev' + - coverage ; extra == 'dev' + - docker ; extra == 'dev' + - flake8 ; extra == 'dev' + - isort ; extra == 'dev' + - mypy ; extra == 'dev' + - pip ; extra == 'dev' + - pre-commit ; extra == 'dev' + - pytest ; extra == 'dev' + - types-click ; extra == 'dev' + - types-filelock ; extra == 'dev' + - types-requests ; extra == 'dev' + requires_python: '>=3.8' - conda: https://conda.anaconda.org/conda-forge/noarch/escapism-1.0.1-pyhd8ed1ab_1.conda sha256: b08de6b98e82b0823026c798ec4f01deca6c85803fea0cbd3072ba40070b41d7 md5: d6302746234eab50a2e7d0ef697f69bf @@ -1728,95 +1493,83 @@ packages: - pkg:pypi/escapism?source=hash-mapping size: 9444 timestamp: 1734956893639 -- conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.2-pyhd8ed1ab_1.conda - sha256: cbde2c64ec317118fc06b223c5fd87c8a680255e7348dd60e7b292d2e103e701 - md5: a16662747cdeb9abbac74d0057cc976e - depends: - - python >=3.9 - license: MIT and PSF-2.0 - purls: - - pkg:pypi/exceptiongroup?source=hash-mapping - size: 20486 - timestamp: 1733208916977 -- conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.1.0-pyhd8ed1ab_1.conda - sha256: 28d25ea375ebab4bf7479228f8430db20986187b04999136ff5c722ebd32eb60 - md5: ef8b5fca76806159fc25b4f48d8737eb - depends: - - python >=3.9 - license: MIT - license_family: MIT - purls: - - pkg:pypi/executing?source=hash-mapping - size: 28348 - timestamp: 1733569440265 -- conda: https://conda.anaconda.org/conda-forge/linux-64/expat-2.7.3-hecca717_0.conda - sha256: c5d573e6831fb41177fb5ae0f1ee09caed55a868ec9887bc80ccc22c3e57b9b4 - md5: c81f6fa1865526f5ab1e6b669b3ee877 - depends: - - __glibc >=2.17,<3.0.a0 - - libexpat 2.7.3 hecca717_0 - - libgcc >=14 - license: MIT - license_family: MIT - purls: [] - size: 143991 - timestamp: 1763549744569 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/expat-2.7.3-hfae3067_0.conda - sha256: a113f31d0d2645e9991ad8685ca5a8699a70ddc314f87317702d6e36fbcfeb88 - md5: b3389e27c0cf1f8df60114cf03ed7575 - depends: - - libexpat 2.7.3 hfae3067_0 - - libgcc >=14 - license: MIT - license_family: MIT - purls: [] - size: 137213 - timestamp: 1763549921101 -- conda: https://conda.anaconda.org/conda-forge/noarch/fastapi-0.115.11-pyh29332c3_0.conda - sha256: 06b93154b2412fd55a235b44580db144334e90cabfec0e57c69155ce35f40962 - md5: a3085f0ade4096d261ecb15208fa5148 - depends: - - python >=3.9 - - starlette >=0.40.0,<0.47.0 - - typing_extensions >=4.8.0 - - pydantic >=1.7.4,!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0 - - email_validator >=2.0.0 - - fastapi-cli >=0.0.5 - - httpx >=0.23.0 - - jinja2 >=3.1.5 - - python-multipart >=0.0.18 - - uvicorn-standard >=0.12.0 - - python - license: MIT - license_family: MIT - purls: - - pkg:pypi/fastapi?source=hash-mapping - size: 78175 - timestamp: 1740930998506 -- conda: https://conda.anaconda.org/conda-forge/noarch/fastapi-cli-0.0.7-pyhd8ed1ab_0.conda - sha256: 300683731013b7221922339cd40430bb3c2ddeeb658fd7e37f5099ffe64e4db0 - md5: d960e0ea9e1c561aa928f6c4439f04c7 - depends: - - python >=3.9 - - rich-toolkit >=0.11.1 - - typer >=0.12.3 - - uvicorn-standard >=0.15.0 - license: MIT - license_family: MIT - purls: - - pkg:pypi/fastapi-cli?source=hash-mapping - size: 15546 - timestamp: 1734302408607 -- conda: https://conda.anaconda.org/conda-forge/noarch/filelock-3.18.0-pyhd8ed1ab_0.conda - sha256: de7b6d4c4f865609ae88db6fa03c8b7544c2452a1aa5451eb7700aad16824570 - md5: 4547b39256e296bb758166893e909a7c - depends: - - python >=3.9 - license: Unlicense - purls: - - pkg:pypi/filelock?source=compressed-mapping - size: 17887 - timestamp: 1741969612334 +- pypi: https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl + name: exceptiongroup + version: 1.3.1 + sha256: a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598 + requires_dist: + - typing-extensions>=4.6.0 ; python_full_version < '3.13' + - pytest>=6 ; extra == 'test' + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl + name: executing + version: 2.2.1 + sha256: 760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017 + requires_dist: + - asttokens>=2.1.0 ; extra == 'tests' + - ipython ; extra == 'tests' + - pytest ; extra == 'tests' + - coverage ; extra == 'tests' + - coverage-enable-subprocess ; extra == 'tests' + - littleutils ; extra == 'tests' + - rich ; python_full_version >= '3.11' and extra == 'tests' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/5a/ff/2e4eca3ade2c22fe1dea7043b8ee9dabe47753349eb1b56a202de8af6349/fastapi-0.136.1-py3-none-any.whl + name: fastapi + version: 0.136.1 + sha256: a6e9d7eeada96c93a4d69cb03836b44fa34e2854accb7244a1ece36cd4781c3f + requires_dist: + - starlette>=0.46.0 + - pydantic>=2.9.0 + - typing-extensions>=4.8.0 + - typing-inspection>=0.4.2 + - annotated-doc>=0.0.2 + - fastapi-cli[standard]>=0.0.8 ; extra == 'standard' + - fastar>=0.9.0 ; extra == 'standard' + - httpx>=0.23.0,<1.0.0 ; extra == 'standard' + - jinja2>=3.1.5 ; extra == 'standard' + - python-multipart>=0.0.18 ; extra == 'standard' + - email-validator>=2.0.0 ; extra == 'standard' + - uvicorn[standard]>=0.12.0 ; extra == 'standard' + - pydantic-settings>=2.0.0 ; extra == 'standard' + - pydantic-extra-types>=2.0.0 ; extra == 'standard' + - fastapi-cli[standard-no-fastapi-cloud-cli]>=0.0.8 ; extra == 'standard-no-fastapi-cloud-cli' + - httpx>=0.23.0,<1.0.0 ; extra == 'standard-no-fastapi-cloud-cli' + - jinja2>=3.1.5 ; extra == 'standard-no-fastapi-cloud-cli' + - python-multipart>=0.0.18 ; extra == 'standard-no-fastapi-cloud-cli' + - email-validator>=2.0.0 ; extra == 'standard-no-fastapi-cloud-cli' + - uvicorn[standard]>=0.12.0 ; extra == 'standard-no-fastapi-cloud-cli' + - pydantic-settings>=2.0.0 ; extra == 'standard-no-fastapi-cloud-cli' + - pydantic-extra-types>=2.0.0 ; extra == 'standard-no-fastapi-cloud-cli' + - fastapi-cli[standard]>=0.0.8 ; extra == 'all' + - httpx>=0.23.0,<1.0.0 ; extra == 'all' + - jinja2>=3.1.5 ; extra == 'all' + - python-multipart>=0.0.18 ; extra == 'all' + - itsdangerous>=1.1.0 ; extra == 'all' + - pyyaml>=5.3.1 ; extra == 'all' + - email-validator>=2.0.0 ; extra == 'all' + - uvicorn[standard]>=0.12.0 ; extra == 'all' + - pydantic-settings>=2.0.0 ; extra == 'all' + - pydantic-extra-types>=2.0.0 ; extra == 'all' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl + name: fastjsonschema + version: 2.21.2 + sha256: 1c797122d0a86c5cace2e54bf4e819c36223b552017172f32c5c024a6b77e463 + requires_dist: + - colorama ; extra == 'devel' + - jsonschema ; extra == 'devel' + - json-spec ; extra == 'devel' + - pylint ; extra == 'devel' + - pytest ; extra == 'devel' + - pytest-benchmark ; extra == 'devel' + - pytest-cache ; extra == 'devel' + - validictory ; extra == 'devel' +- pypi: https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl + name: filelock + version: 3.29.0 + sha256: 96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258 + requires_python: '>=3.10' - conda: https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_1.conda sha256: 2509992ec2fd38ab27c7cdb42cf6cadc566a1cc0d1021a2673475d9fa87c6276 md5: d3549fd50d450b6d9e7dddff25dd2110 @@ -1829,29 +1582,6 @@ packages: - pkg:pypi/fqdn?source=hash-mapping size: 16705 timestamp: 1733327494780 -- conda: https://conda.anaconda.org/conda-forge/linux-64/freetype-2.13.3-h48d6fc4_0.conda - sha256: 7385577509a9c4730130f54bb6841b9b416249d5f4e9f74bf313e6378e313c57 - md5: 9ecfd6f2ca17077dd9c2d24770bb9ccd - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - libpng >=1.6.47,<1.7.0a0 - - libzlib >=1.3.1,<2.0a0 - license: GPL-2.0-only OR FTL - purls: [] - size: 639682 - timestamp: 1741863789964 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/freetype-2.13.3-he93130f_0.conda - sha256: 242eb2a7888d3d624535ec3c488c9db7d4cdfabb6e9ae78beea6abc45d6a4eae - md5: 3743da39462f21956d6429a4a554ff4f - depends: - - libgcc >=13 - - libpng >=1.6.47,<1.7.0a0 - - libzlib >=1.3.1,<2.0a0 - license: GPL-2.0-only OR FTL - purls: [] - size: 648847 - timestamp: 1741863827451 - conda: https://conda.anaconda.org/conda-forge/linux-64/frozenlist-1.5.0-py310h89163eb_1.conda sha256: 5a6142dddd42b3919ba2bd7ca9e2fd5af31516c180a7ec344d0f4795518fab6d md5: 4e8901e0c05f60897ca052a4991c57e4 @@ -1880,71 +1610,114 @@ packages: - pkg:pypi/frozenlist?source=hash-mapping size: 60622 timestamp: 1737645501699 -- conda: https://conda.anaconda.org/conda-forge/noarch/fsspec-2025.3.0-pyhd8ed1ab_0.conda - sha256: 9cbba3b36d1e91e4806ba15141936872d44d20a4d1e3bb74f4aea0ebeb01b205 - md5: 5ecafd654e33d1f2ecac5ec97057593b - depends: - - python >=3.9 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/fsspec?source=hash-mapping - size: 141329 - timestamp: 1741404114588 -- conda: https://conda.anaconda.org/conda-forge/linux-64/gettext-0.25.1-h3f43e3d_1.conda - sha256: cbfa8c80771d1842c2687f6016c5e200b52d4ca8f2cc119f6377f64f899ba4ff - md5: c42356557d7f2e37676e121515417e3b - depends: - - __glibc >=2.17,<3.0.a0 - - gettext-tools 0.25.1 h3f43e3d_1 - - libasprintf 0.25.1 h3f43e3d_1 - - libasprintf-devel 0.25.1 h3f43e3d_1 - - libgcc >=14 - - libgettextpo 0.25.1 h3f43e3d_1 - - libgettextpo-devel 0.25.1 h3f43e3d_1 - - libiconv >=1.18,<2.0a0 - - libstdcxx >=14 - license: LGPL-2.1-or-later AND GPL-3.0-or-later - purls: [] - size: 541357 - timestamp: 1753343006214 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gettext-0.25.1-h5ad3122_0.conda - sha256: 510e7eba15e6ba71cd5a2ae403128d56b3bb990878c8110f3abc652f823b4af8 - md5: 1e99d353785a5302bce1a5a86d249b2b - depends: - - gettext-tools 0.25.1 h5ad3122_0 - - libasprintf 0.25.1 h5e0f5ae_0 - - libasprintf-devel 0.25.1 h5e0f5ae_0 - - libgcc >=13 - - libgettextpo 0.25.1 h5ad3122_0 - - libgettextpo-devel 0.25.1 h5ad3122_0 - - libstdcxx >=13 - license: LGPL-2.1-or-later AND GPL-3.0-or-later - purls: [] - size: 534760 - timestamp: 1751557634743 -- conda: https://conda.anaconda.org/conda-forge/linux-64/gettext-tools-0.25.1-h3f43e3d_1.conda - sha256: c792729288bdd94f21f25f80802d4c66957b4e00a57f7cb20513f07aadfaff06 - md5: a59c05d22bdcbb4e984bf0c021a2a02f - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - - libiconv >=1.18,<2.0a0 - license: GPL-3.0-or-later - license_family: GPL - purls: [] - size: 3644103 - timestamp: 1753342966311 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gettext-tools-0.25.1-h5ad3122_0.conda - sha256: 7b03cc531c9c2d567eb81dffe9f5688c83fbcdfa4882eec3a2045ec43218806f - md5: 4215d91c0eaae5274a36a3f211898c91 - depends: - - libgcc >=13 - license: GPL-3.0-or-later - license_family: GPL - purls: [] - size: 3999301 - timestamp: 1751557600737 +- pypi: https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl + name: fsspec + version: 2026.4.0 + sha256: 11ef7bb35dab8a394fde6e608221d5cf3e8499401c249bebaeaad760a1a8dec2 + requires_dist: + - adlfs ; extra == 'abfs' + - adlfs ; extra == 'adl' + - pyarrow>=1 ; extra == 'arrow' + - dask ; extra == 'dask' + - distributed ; extra == 'dask' + - pre-commit ; extra == 'dev' + - ruff>=0.5 ; extra == 'dev' + - numpydoc ; extra == 'doc' + - sphinx ; extra == 'doc' + - sphinx-design ; extra == 'doc' + - sphinx-rtd-theme ; extra == 'doc' + - yarl ; extra == 'doc' + - dropbox ; extra == 'dropbox' + - dropboxdrivefs ; extra == 'dropbox' + - requests ; extra == 'dropbox' + - adlfs ; extra == 'full' + - aiohttp!=4.0.0a0,!=4.0.0a1 ; extra == 'full' + - dask ; extra == 'full' + - distributed ; extra == 'full' + - dropbox ; extra == 'full' + - dropboxdrivefs ; extra == 'full' + - fusepy ; extra == 'full' + - gcsfs>2024.2.0 ; extra == 'full' + - libarchive-c ; extra == 'full' + - ocifs ; extra == 'full' + - panel ; extra == 'full' + - paramiko ; extra == 'full' + - pyarrow>=1 ; extra == 'full' + - pygit2 ; extra == 'full' + - requests ; extra == 'full' + - s3fs>2024.2.0 ; extra == 'full' + - smbprotocol ; extra == 'full' + - tqdm ; extra == 'full' + - fusepy ; extra == 'fuse' + - gcsfs>2024.2.0 ; extra == 'gcs' + - pygit2 ; extra == 'git' + - requests ; extra == 'github' + - gcsfs ; extra == 'gs' + - panel ; extra == 'gui' + - pyarrow>=1 ; extra == 'hdfs' + - aiohttp!=4.0.0a0,!=4.0.0a1 ; extra == 'http' + - libarchive-c ; extra == 'libarchive' + - ocifs ; extra == 'oci' + - s3fs>2024.2.0 ; extra == 's3' + - paramiko ; extra == 'sftp' + - smbprotocol ; extra == 'smb' + - paramiko ; extra == 'ssh' + - aiohttp!=4.0.0a0,!=4.0.0a1 ; extra == 'test' + - numpy ; extra == 'test' + - pytest ; extra == 'test' + - pytest-asyncio!=0.22.0 ; extra == 'test' + - pytest-benchmark ; extra == 'test' + - pytest-cov ; extra == 'test' + - pytest-mock ; extra == 'test' + - pytest-recording ; extra == 'test' + - pytest-rerunfailures ; extra == 'test' + - requests ; extra == 'test' + - aiobotocore>=2.5.4,<3.0.0 ; extra == 'test-downstream' + - dask[dataframe,test] ; extra == 'test-downstream' + - moto[server]>4,<5 ; extra == 'test-downstream' + - pytest-timeout ; extra == 'test-downstream' + - xarray ; extra == 'test-downstream' + - adlfs ; extra == 'test-full' + - aiohttp!=4.0.0a0,!=4.0.0a1 ; extra == 'test-full' + - backports-zstd ; python_full_version < '3.14' and extra == 'test-full' + - cloudpickle ; extra == 'test-full' + - dask ; extra == 'test-full' + - distributed ; extra == 'test-full' + - dropbox ; extra == 'test-full' + - dropboxdrivefs ; extra == 'test-full' + - fastparquet ; extra == 'test-full' + - fusepy ; extra == 'test-full' + - gcsfs ; extra == 'test-full' + - jinja2 ; extra == 'test-full' + - kerchunk ; extra == 'test-full' + - libarchive-c ; extra == 'test-full' + - lz4 ; extra == 'test-full' + - notebook ; extra == 'test-full' + - numpy ; extra == 'test-full' + - ocifs ; extra == 'test-full' + - pandas<3.0.0 ; extra == 'test-full' + - panel ; extra == 'test-full' + - paramiko ; extra == 'test-full' + - pyarrow ; extra == 'test-full' + - pyarrow>=1 ; extra == 'test-full' + - pyftpdlib ; extra == 'test-full' + - pygit2 ; extra == 'test-full' + - pytest ; extra == 'test-full' + - pytest-asyncio!=0.22.0 ; extra == 'test-full' + - pytest-benchmark ; extra == 'test-full' + - pytest-cov ; extra == 'test-full' + - pytest-mock ; extra == 'test-full' + - pytest-recording ; extra == 'test-full' + - pytest-rerunfailures ; extra == 'test-full' + - python-snappy ; extra == 'test-full' + - requests ; extra == 'test-full' + - smbprotocol ; extra == 'test-full' + - tqdm ; extra == 'test-full' + - urllib3 ; extra == 'test-full' + - zarr ; extra == 'test-full' + - zstandard ; python_full_version < '3.14' and extra == 'test-full' + - tqdm ; extra == 'tqdm' + requires_python: '>=3.10' - conda: https://conda.anaconda.org/conda-forge/linux-64/git-2.52.0-pl5321h6d3cee1_1.conda sha256: 213eda4680ff80c59a146af0a664c4f3ee207c87e478ef323c7147dd5becacd3 md5: 815606e45cf1c006ba346a6ca9e9eb9c @@ -1979,31 +1752,35 @@ packages: purls: [] size: 13827753 timestamp: 1763720065676 -- conda: https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.12-pyhd8ed1ab_0.conda - sha256: dbbec21a369872c8ebe23cb9a3b9d63638479ee30face165aa0fccc96e93eec3 - md5: 7c14f3706e099f8fcd47af2d494616cc - depends: - - python >=3.9 - - smmap >=3.0.1,<6 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/gitdb?source=hash-mapping - size: 53136 - timestamp: 1735887290843 -- conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.44-pyhff2d567_0.conda - sha256: b996e717ca693e4e831d3d3143aca3abb47536561306195002b226fe4dde53c3 - md5: 140a4e944f7488467872e562a2a52789 - depends: - - gitdb >=4.0.1,<5 - - python >=3.9 - - typing_extensions >=3.7.4.3 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/gitpython?source=hash-mapping - size: 157200 - timestamp: 1735929768433 +- pypi: https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl + name: gitdb + version: 4.0.12 + sha256: 67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf + requires_dist: + - smmap>=3.0.1,<6 + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-py3-none-any.whl + name: gitpython + version: 3.1.50 + sha256: d352abe2908d07355014abdd21ddf798c2a961469239afec4962e9da884858f9 + requires_dist: + - gitdb>=4.0.1,<5 + - typing-extensions>=3.10.0.2 ; python_full_version < '3.10' + - coverage[toml] ; extra == 'test' + - ddt>=1.1.1,!=1.4.3 ; extra == 'test' + - mock ; python_full_version < '3.8' and extra == 'test' + - mypy==1.18.2 ; python_full_version >= '3.9' and extra == 'test' + - pre-commit ; extra == 'test' + - pytest>=7.3.1 ; extra == 'test' + - pytest-cov ; extra == 'test' + - pytest-instafail ; extra == 'test' + - pytest-mock ; extra == 'test' + - pytest-sugar ; extra == 'test' + - typing-extensions ; python_full_version < '3.11' and extra == 'test' + - sphinx>=7.4.7,<8 ; extra == 'doc' + - sphinx-rtd-theme ; extra == 'doc' + - sphinx-autodoc-typehints ; extra == 'doc' + requires_python: '>=3.7' - conda: https://conda.anaconda.org/conda-forge/noarch/google-auth-2.38.0-pyhd8ed1ab_0.conda sha256: 0bbff264a2a50af0e2a61a4445c1b2353c6f44d87b83ffb36c95cca5d8fd4aaa md5: c48abda87ffa7a0cc9f819cb8a384a9a @@ -2053,184 +1830,83 @@ packages: - pkg:pypi/greenlet?source=hash-mapping size: 216950 timestamp: 1734533013975 -- conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.14.0-pyhd8ed1ab_1.conda - sha256: 622516185a7c740d5c7f27016d0c15b45782c1501e5611deec63fd70344ce7c8 - md5: 7ee49e89531c0dcbba9466f6d115d585 - depends: - - python >=3.9 - - typing_extensions - license: MIT - license_family: MIT - purls: - - pkg:pypi/h11?source=hash-mapping - size: 51846 - timestamp: 1733327599467 -- conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda - sha256: 0aa1cdc67a9fe75ea95b5644b734a756200d6ec9d0dff66530aec3d1c1e9df75 - md5: b4754fb1bdcb70c8fd54f918301582c6 - depends: - - hpack >=4.1,<5 - - hyperframe >=6.1,<7 - - python >=3.9 - license: MIT - license_family: MIT - purls: - - pkg:pypi/h2?source=hash-mapping - size: 53888 - timestamp: 1738578623567 -- conda: https://conda.anaconda.org/conda-forge/noarch/hatch-1.14.0-pyhd8ed1ab_1.conda - sha256: 908fc6e847da57da39011be839db0f56f16428d3d950f49a8c7ac1c9c1ed0505 - md5: b34bdd91d7298c76d9891cea6c8ab27f - depends: - - click >=8.0.6 - - hatchling >=1.24.2 - - httpx >=0.22.0 - - hyperlink >=21.0.0 - - keyring >=23.5.0 - - packaging >=24.2 - - pexpect >=4.8,<5 - - platformdirs >=2.5.0 - - python >=3.9 - - rich >=11.2.0 - - shellingham >=1.4.0 - - tomli-w >=1.0 - - tomlkit >=0.11.1 - - userpath >=1.7,<2 - - uv >=0.1.35 - - virtualenv >=20.26.6 - - zstandard <1.0 - license: MIT - license_family: MIT - purls: - - pkg:pypi/hatch?source=hash-mapping - size: 177914 - timestamp: 1734476985835 -- conda: https://conda.anaconda.org/conda-forge/noarch/hatchling-1.27.0-pypyhd8ed1ab_0.conda - sha256: e83420f81390535774ac33b83d05249b8993e5376b76b4d461f83a77549e493d - md5: b85c18ba6e927ae0da3fde426c893cc8 - depends: - - editables >=0.3 - - importlib-metadata - - packaging >=21.3 - - pathspec >=0.10.1 - - pluggy >=1.0.0 - - python >=3.7 - - python >=3.8 - - tomli >=1.2.2 +- pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl + name: h11 + version: 0.16.0 + sha256: 63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/6b/29/742010d61a7665b863a36208bfa3df93476e9a86fde45413cd13db76f7d0/hatch-1.16.4-py3-none-any.whl + name: hatch + version: 1.16.4 + sha256: 0b434f522a5b1b0303ffe6195ad056b7f815403413c72561308001fde82a3b6f + requires_dist: + - backports-zstd>=1.0.0 ; python_full_version < '3.14' + - click>=8.0.6 + - hatchling>=1.27.0 + - httpx>=0.22.0 + - hyperlink>=21.0.0 + - keyring>=23.5.0 + - packaging>=24.2 + - pexpect~=4.8 + - platformdirs>=2.5.0 + - pyproject-hooks + - rich>=11.2.0 + - shellingham>=1.4.0 + - tomli-w>=1.0 + - tomlkit>=0.11.1 + - userpath~=1.7 + - uv>=0.5.23 + - virtualenv>=20.26.6 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/d3/8a/44032265776062a89171285ede55a0bdaadc8ac00f27f0512a71a9e3e1c8/hatchling-1.29.0-py3-none-any.whl + name: hatchling + version: 1.29.0 + sha256: 50af9343281f34785fab12da82e445ed987a6efb34fd8c2fc0f6e6630dbcc1b0 + requires_dist: + - packaging>=24.2 + - pathspec>=0.10.1 + - pluggy>=1.0.0 + - tomli>=1.2.2 ; python_full_version < '3.11' - trove-classifiers - license: MIT - license_family: MIT - purls: - - pkg:pypi/hatchling?source=hash-mapping - size: 56598 - timestamp: 1734311718682 -- conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda - sha256: 6ad78a180576c706aabeb5b4c8ceb97c0cb25f1e112d76495bff23e3779948ba - md5: 0a802cb9888dd14eeefc611f05c40b6e - depends: - - python >=3.9 - license: MIT - license_family: MIT - purls: - - pkg:pypi/hpack?source=hash-mapping - size: 30731 - timestamp: 1737618390337 -- conda: https://conda.anaconda.org/conda-forge/noarch/html5lib-1.1-pyhd8ed1ab_2.conda - sha256: 8027e436ad59e2a7392f6036392ef9d6c223798d8a1f4f12d5926362def02367 - md5: cf25bfddbd3bc275f3d3f9936cee1dd3 - depends: - - python >=3.9 - - six >=1.9 - - webencodings - license: MIT - license_family: MIT - purls: - - pkg:pypi/html5lib?source=hash-mapping - size: 94853 - timestamp: 1734075276288 -- conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.7-pyh29332c3_1.conda - sha256: c84d012a245171f3ed666a8bf9319580c269b7843ffa79f26468842da3abd5df - md5: 2ca8e6dbc86525c8b95e3c0ffa26442e - depends: - - python >=3.8 - - h11 >=0.13,<0.15 - - h2 >=3,<5 - - sniffio 1.* - - anyio >=3.0,<5.0 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl + name: httpcore + version: 1.0.9 + sha256: 2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 + requires_dist: - certifi - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/httpcore?source=hash-mapping - size: 48959 - timestamp: 1731707562362 -- conda: https://conda.anaconda.org/conda-forge/linux-64/httptools-0.6.4-py310ha75aee5_0.conda - sha256: dc62022f9fb5ee82eb3274e44ce842f4842ab7ecc38375b38ace065df7bbaf13 - md5: 33f0fb2d3851f38bd3feddbebfa8e76d - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - python >=3.10,<3.11.0a0 - - python_abi 3.10.* *_cp310 - license: MIT - license_family: MIT - purls: - - pkg:pypi/httptools?source=hash-mapping - size: 99044 - timestamp: 1732707759185 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/httptools-0.6.4-py310ha766c32_0.conda - sha256: f69d2b417e0640224962d41cd0c28a5cdc9f6093a00679a94d5e17adaa3b2cec - md5: 79272616743c44ddae159bb6058957d9 - depends: - - libgcc >=13 - - python >=3.10,<3.11.0a0 - - python >=3.10,<3.11.0a0 *_cpython - - python_abi 3.10.* *_cp310 - license: MIT - license_family: MIT - purls: - - pkg:pypi/httptools?source=hash-mapping - size: 98391 - timestamp: 1732707895063 -- conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda - sha256: cd0f1de3697b252df95f98383e9edb1d00386bfdd03fdf607fa42fe5fcb09950 - md5: d6989ead454181f4f9bc987d3dc4e285 - depends: + - h11>=0.16 + - anyio>=4.0,<5.0 ; extra == 'asyncio' + - h2>=3,<5 ; extra == 'http2' + - socksio==1.* ; extra == 'socks' + - trio>=0.22.0,<1.0 ; extra == 'trio' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl + name: httpx + version: 0.28.1 + sha256: d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad + requires_dist: - anyio - certifi - - httpcore 1.* + - httpcore==1.* - idna - - python >=3.9 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/httpx?source=hash-mapping - size: 63082 - timestamp: 1733663449209 -- conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - sha256: 77af6f5fe8b62ca07d09ac60127a30d9069fdc3c68d6b256754d0ffb1f7779f8 - md5: 8e6923fc12f1fe8f8c4e5c9f343256ac - depends: - - python >=3.9 - license: MIT - license_family: MIT - purls: - - pkg:pypi/hyperframe?source=hash-mapping - size: 17397 - timestamp: 1737618427549 -- conda: https://conda.anaconda.org/conda-forge/noarch/hyperlink-21.0.0-pyh29332c3_1.conda - sha256: 6fc0a91c590b3055bfb7983e6521c7b780ab8b11025058eaf898049ea827d829 - md5: c27acdecaf3c311e5781b81fe02d9641 - depends: - - python >=3.9 - - idna >=2.6 - - python - license: MIT - license_family: MIT - purls: - - pkg:pypi/hyperlink?source=hash-mapping - size: 74751 - timestamp: 1733319972207 + - brotli ; platform_python_implementation == 'CPython' and extra == 'brotli' + - brotlicffi ; platform_python_implementation != 'CPython' and extra == 'brotli' + - click==8.* ; extra == 'cli' + - pygments==2.* ; extra == 'cli' + - rich>=10,<14 ; extra == 'cli' + - h2>=3,<5 ; extra == 'http2' + - socksio==1.* ; extra == 'socks' + - zstandard>=0.18.0 ; extra == 'zstd' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/6e/aa/8caf6a0a3e62863cbb9dab27135660acba46903b703e224f14f447e57934/hyperlink-21.0.0-py2.py3-none-any.whl + name: hyperlink + version: 21.0.0 + sha256: e6b14c37ecb73e89c77d78cdb4c2cc8f3fb59a885c5b3f819ff4ed80f25af1b4 + requires_dist: + - idna>=2.5 + - typing ; python_full_version < '3.5' + requires_python: '>=2.6,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*' - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda sha256: 71e750d509f5fa3421087ba88ef9a7b9be11c53174af3aa4d06aff4c18b38e8e md5: 8b189310083baabfb622af68fd9d3ae3 @@ -2291,70 +1967,120 @@ packages: - pkg:pypi/importlib-resources?source=hash-mapping size: 33781 timestamp: 1736252433366 -- conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-6.29.5-pyh3099207_0.conda - sha256: 33cfd339bb4efac56edf93474b37ddc049e08b1b4930cf036c893cc1f5a1f32a - md5: b40131ab6a36ac2c09b7c57d4d3fbf99 - depends: - - __linux - - comm >=0.1.1 - - debugpy >=1.6.5 - - ipython >=7.23.1 - - jupyter_client >=6.1.12 - - jupyter_core >=4.12,!=5.0.* - - matplotlib-inline >=0.1 - - nest-asyncio - - packaging - - psutil - - python >=3.8 - - pyzmq >=24 - - tornado >=6.1 - - traitlets >=5.4.0 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/ipykernel?source=hash-mapping - size: 119084 - timestamp: 1719845605084 -- conda: https://conda.anaconda.org/conda-forge/noarch/ipython-8.34.0-pyh907856f_0.conda - sha256: de98e198c269191b114b1a9806af31dd26dd11ac313f3479e95a4ddf952b5566 - md5: 1a5e5b082a5bc8561510ddb0a8ba9ac3 - depends: - - __unix - - pexpect >4.3 +- pypi: https://files.pythonhosted.org/packages/e5/ca/1172b6638d52f2d6caa2dd262ec4c811ba59eee96d54a7701930726bce18/installer-0.7.0-py3-none-any.whl + name: installer + version: 0.7.0 + sha256: 05d1933f0a5ba7d8d6296bb6d5018e7c94fa473ceb10cf198a92ccea19c27b53 + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/82/b9/e73d5d9f405cba7706c539aa8b311b49d4c2f3d698d9c12f815231169c71/ipykernel-7.2.0-py3-none-any.whl + name: ipykernel + version: 7.2.0 + sha256: 3bbd4420d2b3cc105cbdf3756bfc04500b1e52f090a90716851f3916c62e1661 + requires_dist: + - appnope>=0.1.2 ; sys_platform == 'darwin' + - comm>=0.1.1 + - debugpy>=1.6.5 + - ipython>=7.23.1 + - jupyter-client>=8.8.0 + - jupyter-core>=5.1,!=6.0.* + - matplotlib-inline>=0.1 + - nest-asyncio>=1.4 + - packaging>=22 + - psutil>=5.7 + - pyzmq>=25 + - tornado>=6.4.1 + - traitlets>=5.4.0 + - coverage[toml] ; extra == 'cov' + - matplotlib ; extra == 'cov' + - pytest-cov ; extra == 'cov' + - trio ; extra == 'cov' + - intersphinx-registry ; extra == 'docs' + - myst-parser ; extra == 'docs' + - pydata-sphinx-theme ; extra == 'docs' + - sphinx-autodoc-typehints ; extra == 'docs' + - sphinx<8.2.0 ; extra == 'docs' + - sphinxcontrib-github-alt ; extra == 'docs' + - sphinxcontrib-spelling ; extra == 'docs' + - trio ; extra == 'docs' + - pyqt5 ; extra == 'pyqt5' + - pyside6 ; extra == 'pyside6' + - flaky ; extra == 'test' + - ipyparallel ; extra == 'test' + - pre-commit ; extra == 'test' + - pytest-asyncio>=0.23.5 ; extra == 'test' + - pytest-cov ; extra == 'test' + - pytest-timeout ; extra == 'test' + - pytest>=7.0,<10 ; extra == 'test' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/c0/56/4cc7fc9e9e3f38fd324f24f8afe0ad8bb5fa41283f37f1aaf9de0612c968/ipython-8.39.0-py3-none-any.whl + name: ipython + version: 8.39.0 + sha256: bb3c51c4fa8148ab1dea07a79584d1c854e234ea44aa1283bcb37bc75054651f + requires_dist: + - colorama ; sys_platform == 'win32' - decorator - - exceptiongroup - - jedi >=0.16 + - exceptiongroup ; python_full_version < '3.11' + - jedi>=0.16 - matplotlib-inline - - pickleshare - - prompt-toolkit >=3.0.41,<3.1.0 - - pygments >=2.4.0 - - python >=3.10 - - stack_data - - traitlets >=5.13.0 - - typing_extensions >=4.6 - - python - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/ipython?source=compressed-mapping - size: 634948 - timestamp: 1741457802509 -- conda: https://conda.anaconda.org/conda-forge/noarch/ipywidgets-8.1.5-pyhd8ed1ab_1.conda - sha256: f419657566e3d9bea85b288a0ce3a8e42d76cd82ac1697c6917891df3ae149ab - md5: bb19ad65196475ab6d0bb3532d7f8d96 - depends: - - comm >=0.1.3 - - ipython >=6.1.0 - - jupyterlab_widgets >=3.0.13,<3.1.0 - - python >=3.9 - - traitlets >=4.3.1 - - widgetsnbextension >=4.0.13,<4.1.0 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/ipywidgets?source=hash-mapping - size: 113982 - timestamp: 1733493669268 + - pexpect>4.3 ; sys_platform != 'emscripten' and sys_platform != 'win32' + - prompt-toolkit>=3.0.41,<3.1.0 + - pygments>=2.4.0 + - stack-data + - traitlets>=5.13.0 + - typing-extensions>=4.6 ; python_full_version < '3.12' + - black ; extra == 'black' + - docrepr ; extra == 'doc' + - exceptiongroup ; extra == 'doc' + - intersphinx-registry ; extra == 'doc' + - ipykernel ; extra == 'doc' + - ipython[test] ; extra == 'doc' + - matplotlib ; extra == 'doc' + - setuptools>=18.5 ; extra == 'doc' + - sphinx-rtd-theme ; extra == 'doc' + - sphinx>=1.3 ; extra == 'doc' + - sphinxcontrib-jquery ; extra == 'doc' + - tomli ; python_full_version < '3.11' and extra == 'doc' + - typing-extensions ; extra == 'doc' + - ipykernel ; extra == 'kernel' + - nbconvert ; extra == 'nbconvert' + - nbformat ; extra == 'nbformat' + - ipywidgets ; extra == 'notebook' + - notebook ; extra == 'notebook' + - ipyparallel ; extra == 'parallel' + - qtconsole ; extra == 'qtconsole' + - pytest ; extra == 'test' + - pytest-asyncio<0.22 ; extra == 'test' + - testpath ; extra == 'test' + - pickleshare ; extra == 'test' + - packaging ; extra == 'test' + - ipython[test] ; extra == 'test-extra' + - curio ; extra == 'test-extra' + - jupyter-ai ; extra == 'test-extra' + - matplotlib!=3.2.0 ; extra == 'test-extra' + - nbformat ; extra == 'test-extra' + - numpy>=1.23 ; extra == 'test-extra' + - pandas ; extra == 'test-extra' + - trio ; extra == 'test-extra' + - matplotlib ; extra == 'matplotlib' + - ipython[black,doc,kernel,matplotlib,nbconvert,nbformat,notebook,parallel,qtconsole] ; extra == 'all' + - ipython[test,test-extra] ; extra == 'all' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-py3-none-any.whl + name: ipywidgets + version: 8.1.8 + sha256: ecaca67aed704a338f88f67b1181b58f821ab5dc89c1f0f5ef99db43c1c2921e + requires_dist: + - comm>=0.1.3 + - ipython>=6.1.0 + - traitlets>=4.3.1 + - widgetsnbextension~=4.0.14 + - jupyterlab-widgets~=3.0.15 + - jsonschema ; extra == 'test' + - ipykernel ; extra == 'test' + - pytest>=3.6.0 ; extra == 'test' + - pytest-cov ; extra == 'test' + - pytz ; extra == 'test' + requires_python: '>=3.7' - conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_1.conda sha256: 08e838d29c134a7684bca0468401d26840f41c92267c4126d7b43a6b533b0aed md5: 0b0154421989637d424ccf0f104be51a @@ -2367,94 +2093,157 @@ packages: - pkg:pypi/isoduration?source=hash-mapping size: 19832 timestamp: 1733493720346 -- conda: https://conda.anaconda.org/conda-forge/noarch/jaraco.classes-3.4.0-pyhd8ed1ab_2.conda - sha256: 3d16a0fa55a29fe723c918a979b2ee927eb0bf9616381cdfd26fa9ea2b649546 - md5: ade6b25a6136661dadd1a43e4350b10b - depends: +- pypi: https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl + name: jaraco-classes + version: 3.4.0 + sha256: f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790 + requires_dist: - more-itertools - - python >=3.9 - license: MIT - license_family: MIT - purls: - - pkg:pypi/jaraco-classes?source=hash-mapping - size: 12109 - timestamp: 1733326001034 -- conda: https://conda.anaconda.org/conda-forge/noarch/jaraco.context-6.0.1-pyhd8ed1ab_0.conda - sha256: bfaba92cd33a0ae2488ab64a1d4e062bcf52b26a71f88292c62386ccac4789d7 - md5: bcc023a32ea1c44a790bbf1eae473486 - depends: - - backports.tarfile - - python >=3.9 - license: MIT - license_family: MIT - purls: - - pkg:pypi/jaraco-context?source=hash-mapping - size: 12483 - timestamp: 1733382698758 -- conda: https://conda.anaconda.org/conda-forge/noarch/jaraco.functools-4.1.0-pyhd8ed1ab_0.conda - sha256: 61da3e37149da5c8479c21571eaec61cc4a41678ee872dcb2ff399c30878dddb - md5: eb257d223050a5a27f5fdf5c9debc8ec - depends: + - sphinx>=3.5 ; extra == 'docs' + - jaraco-packaging>=9.3 ; extra == 'docs' + - rst-linker>=1.9 ; extra == 'docs' + - furo ; extra == 'docs' + - sphinx-lint ; extra == 'docs' + - jaraco-tidelift>=1.4 ; extra == 'docs' + - pytest>=6 ; extra == 'testing' + - pytest-checkdocs>=2.4 ; extra == 'testing' + - pytest-cov ; extra == 'testing' + - pytest-mypy ; extra == 'testing' + - pytest-enabler>=2.2 ; extra == 'testing' + - pytest-ruff>=0.2.1 ; extra == 'testing' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl + name: jaraco-context + version: 6.1.2 + sha256: bf8150b79a2d5d91ae48629d8b427a8f7ba0e1097dd6202a9059f29a36379535 + requires_dist: + - backports-tarfile ; python_full_version < '3.12' + - pytest>=6,!=8.1.* ; extra == 'test' + - jaraco-test>=5.6.0 ; extra == 'test' + - portend ; extra == 'test' + - sphinx>=3.5 ; extra == 'doc' + - jaraco-packaging>=9.3 ; extra == 'doc' + - rst-linker>=1.9 ; extra == 'doc' + - furo ; extra == 'doc' + - sphinx-lint ; extra == 'doc' + - jaraco-tidelift>=1.4 ; extra == 'doc' + - pytest-checkdocs>=2.14 ; extra == 'check' + - pytest-ruff>=0.2.1 ; sys_platform != 'cygwin' and extra == 'check' + - pytest-cov ; extra == 'cover' + - pytest-enabler>=3.4 ; extra == 'enabler' + - pytest-mypy>=1.0.1 ; platform_python_implementation != 'PyPy' and extra == 'type' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl + name: jaraco-functools + version: 4.4.0 + sha256: 9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176 + requires_dist: - more-itertools - - python >=3.9 - license: MIT - license_family: MIT - purls: - - pkg:pypi/jaraco-functools?source=hash-mapping - size: 15545 - timestamp: 1733746481844 -- conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda - sha256: 92c4d217e2dc68983f724aa983cca5464dcb929c566627b26a2511159667dba8 - md5: a4f4c5dc9b80bc50e0d3dc4e6e8f1bd9 - depends: - - parso >=0.8.3,<0.9.0 - - python >=3.9 - license: Apache-2.0 AND MIT - purls: - - pkg:pypi/jedi?source=hash-mapping - size: 843646 - timestamp: 1733300981994 -- conda: https://conda.anaconda.org/conda-forge/noarch/jeepney-0.9.0-pyhd8ed1ab_0.conda - sha256: 00d37d85ca856431c67c8f6e890251e7cc9e5ef3724a0302b8d4a101f22aa27f - md5: b4b91eb14fbe2f850dd2c5fc20676c0d - depends: - - python >=3.9 - license: MIT - license_family: MIT - purls: - - pkg:pypi/jeepney?source=hash-mapping - size: 40015 - timestamp: 1740828380668 -- conda: https://conda.anaconda.org/conda-forge/noarch/jhub-apps-2025.11.1-pyhcf101f3_0.conda - sha256: ed72689b0c1a6b26a981434f34a42644eb4fbb99e402a3e9fc36591f0abca1a4 - md5: e895cd1d55e898d6ba573b53704ec4ea - depends: - - python >=3.10,<4.0 - - hatchling - - hatch - - requests + - pytest>=6,!=8.1.* ; extra == 'test' + - jaraco-classes ; extra == 'test' + - sphinx>=3.5 ; extra == 'doc' + - jaraco-packaging>=9.3 ; extra == 'doc' + - rst-linker>=1.9 ; extra == 'doc' + - furo ; extra == 'doc' + - sphinx-lint ; extra == 'doc' + - jaraco-tidelift>=1.4 ; extra == 'doc' + - pytest-checkdocs>=2.4 ; extra == 'check' + - pytest-ruff>=0.2.1 ; sys_platform != 'cygwin' and extra == 'check' + - pytest-cov ; extra == 'cover' + - pytest-enabler>=3.4 ; extra == 'enabler' + - pytest-mypy>=1.0.1 ; extra == 'type' + - mypy<1.19 ; platform_python_implementation == 'PyPy' and extra == 'type' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/9a/93/242e2eab5fe682ffcb8b0084bde703a41d51e17ee0f3a31ff0d9d813620a/jedi-0.20.0-py2.py3-none-any.whl + name: jedi + version: 0.20.0 + sha256: 7bdd9c2634f56713299976f4cbd59cb3fa92165cc5e05ea811fb253480728b67 + requires_dist: + - parso>=0.8.6,<0.9.0 + - django ; extra == 'dev' + - attrs ; extra == 'dev' + - colorama ; extra == 'dev' + - docopt ; extra == 'dev' + - flake8==7.1.2 ; extra == 'dev' + - pytest<9.0.0 ; extra == 'dev' + - types-setuptools==80.9.0.20250529 ; extra == 'dev' + - typing-extensions ; extra == 'dev' + - zuban==0.7.0 ; extra == 'dev' + - jinja2==3.1.6 ; extra == 'docs' + - markupsafe==3.0.3 ; extra == 'docs' + - pygments==2.20.0 ; extra == 'docs' + - sphinx==9.1.0 ; extra == 'docs' + - alabaster==1.0.0 ; extra == 'docs' + - babel==2.18.0 ; extra == 'docs' + - certifi==2026.4.22 ; extra == 'docs' + - charset-normalizer==3.4.7 ; extra == 'docs' + - docutils==0.22.4 ; extra == 'docs' + - idna==3.13 ; extra == 'docs' + - imagesize==2.0.0 ; extra == 'docs' + - iniconfig==2.3.0 ; extra == 'docs' + - packaging==26.2 ; extra == 'docs' + - pluggy==1.6.0 ; extra == 'docs' + - pytest==9.0.3 ; extra == 'docs' + - requests==2.33.1 ; extra == 'docs' + - roman-numerals==4.1.0 ; extra == 'docs' + - snowballstemmer==3.0.1 ; extra == 'docs' + - sphinx-rtd-theme==3.1.0 ; extra == 'docs' + - sphinxcontrib-applehelp==2.0.0 ; extra == 'docs' + - sphinxcontrib-devhelp==2.0.0 ; extra == 'docs' + - sphinxcontrib-htmlhelp==2.1.0 ; extra == 'docs' + - sphinxcontrib-jquery==4.1 ; extra == 'docs' + - sphinxcontrib-jsmath==1.0.1 ; extra == 'docs' + - sphinxcontrib-qthelp==2.0.0 ; extra == 'docs' + - sphinxcontrib-serializinghtml==2.0.0 ; extra == 'docs' + - urllib3==2.6.3 ; extra == 'docs' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl + name: jeepney + version: 0.9.0 + sha256: 97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683 + requires_dist: + - pytest ; extra == 'test' + - pytest-trio ; extra == 'test' + - pytest-asyncio>=0.17 ; extra == 'test' + - testpath ; extra == 'test' + - trio ; extra == 'test' + - async-timeout ; python_full_version < '3.11' and extra == 'test' + - trio ; extra == 'trio' + requires_python: '>=3.7' +- pypi: git+https://github.com/nebari-dev/jhub-apps.git?rev=a94b049a9bcc72e2844d5cf0cc2978d4badd2cd5#a94b049a9bcc72e2844d5cf0cc2978d4badd2cd5 + name: jhub-apps + version: 2025.11.1 + requires_dist: + - bokeh + - bokeh-root-cmd + - cachetools + - conda-project==0.4.2 - fastapi - - uvicorn - - python-multipart - - jupyterhub >4 + - gitpython + - hatch + - hatchling - jupyter - - plotlydash-tornado-cmd - - bokeh-root-cmd + - jupyterhub>4 - panel - - bokeh - - traitlets + - plotlydash-tornado-cmd + - pyjwt>=2.10.0 + - python-multipart - python-slugify - - cachetools + - requests - structlog - - pyjwt <2.10.0 - - gitpython - - conda-project ==0.4.2 - - python - license: BSD-3-Clause - purls: - - pkg:pypi/jhub-apps?source=hash-mapping - size: 2447582 - timestamp: 1764243640487 + - traitlets + - uvicorn + - dash ; extra == 'dev' + - gradio ; extra == 'dev' + - ipdb ; extra == 'dev' + - playwright ; extra == 'dev' + - pre-commit ; extra == 'dev' + - pytest ; extra == 'dev' + - pytest-playwright ; extra == 'dev' + - ruff ; extra == 'dev' + - streamlit ; extra == 'dev' + - voila ; extra == 'dev' + requires_python: '>=3.9' - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhd8ed1ab_0.conda sha256: f1ac18b11637ddadc05642e8185a851c7fab5998c6f5470d716812fae943b2af md5: 446bd6c8cb26050d528881df495ce646 @@ -2467,17 +2256,11 @@ packages: - pkg:pypi/jinja2?source=compressed-mapping size: 112714 timestamp: 1741263433881 -- conda: https://conda.anaconda.org/conda-forge/noarch/json5-0.10.0-pyhd8ed1ab_1.conda - sha256: 61bca2dac194c44603446944745566d7b4e55407280f6f6cea8bbe4de26b558f - md5: cd170f82d8e5b355dfdea6adab23e4af - depends: - - python >=3.9 - license: Apache-2.0 - license_family: APACHE - purls: - - pkg:pypi/json5?source=hash-mapping - size: 31573 - timestamp: 1733272196759 +- pypi: https://files.pythonhosted.org/packages/b8/42/cf027b4ac873b076189d935b135397675dac80cb29acb13e1ab86ad6c631/json5-0.14.0-py3-none-any.whl + name: json5 + version: 0.14.0 + sha256: 56cf861bab076b1178eb8c92e1311d273a9b9acea2ccc82c276abf839ebaef3a + requires_python: '>=3.8.0' - conda: https://conda.anaconda.org/conda-forge/linux-64/jsonpointer-3.0.0-py310hff52083_1.conda sha256: ac8e92806a5017740b9a1113f0cab8559cd33884867ec7e99b556eb2fa847690 md5: ce614a01b0aee1b29cee13d606bcb5d5 @@ -2550,86 +2333,164 @@ packages: purls: [] size: 7135 timestamp: 1733472820035 -- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-1.1.1-pyhd8ed1ab_1.conda - sha256: b538e15067d05768d1c0532a6d9b0625922a1cce751dd6a2af04f7233a1a70e9 - md5: 9453512288d20847de4356327d0e1282 - depends: +- pypi: https://files.pythonhosted.org/packages/38/64/285f20a31679bf547b75602702f7800e74dbabae36ef324f716c02804753/jupyter-1.1.1-py2.py3-none-any.whl + name: jupyter + version: 1.1.1 + sha256: 7a59533c22af65439b24bbe60373a4e95af8f16ac65a6c00820ad378e3f7cc83 + requires_dist: + - notebook + - jupyter-console + - nbconvert - ipykernel - ipywidgets - - jupyter_console - jupyterlab - - nbconvert-core - - notebook - - python >=3.9 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/jupyter?source=hash-mapping - size: 8891 - timestamp: 1733818677113 -- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.2.5-pyhd8ed1ab_1.conda - sha256: 1565c8b1423a37fca00fe0ab2a17cd8992c2ecf23e7867a1c9f6f86a9831c196 - md5: 0b4c3908e5a38ea22ebb98ee5888c768 - depends: - - importlib-metadata >=4.8.3 - - jupyter_server >=1.1.2 - - python >=3.9 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/jupyter-lsp?source=hash-mapping - size: 55221 - timestamp: 1733493006611 -- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.6.3-pyhd8ed1ab_1.conda - sha256: 19d8bd5bb2fde910ec59e081eeb59529491995ce0d653a5209366611023a0b3a - md5: 4ebae00eae9705b0c3d6d1018a81d047 - depends: - - importlib-metadata >=4.8.3 - - jupyter_core >=4.12,!=5.0.* - - python >=3.9 - - python-dateutil >=2.8.2 - - pyzmq >=23.0 - - tornado >=6.2 - - traitlets >=5.3 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/jupyter-client?source=hash-mapping - size: 106342 - timestamp: 1733441040958 -- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_console-6.6.3-pyhd8ed1ab_1.conda - sha256: aee0cdd0cb2b9321d28450aec4e0fd43566efcd79e862d70ce49a68bf0539bcd - md5: 801dbf535ec26508fac6d4b24adfb76e - depends: - - ipykernel >=6.14 +- pypi: https://files.pythonhosted.org/packages/2d/0b/ceb7694d864abc0a047649aec263878acb9f792e1fec3e676f22dc9015e3/jupyter_client-8.8.0-py3-none-any.whl + name: jupyter-client + version: 8.8.0 + sha256: f93a5b99c5e23a507b773d3a1136bd6e16c67883ccdbd9a829b0bbdb98cd7d7a + requires_dist: + - jupyter-core>=5.1 + - python-dateutil>=2.8.2 + - pyzmq>=25.0 + - tornado>=6.4.1 + - traitlets>=5.3 + - ipykernel ; extra == 'docs' + - myst-parser ; extra == 'docs' + - pydata-sphinx-theme ; extra == 'docs' + - sphinx-autodoc-typehints ; extra == 'docs' + - sphinx>=4 ; extra == 'docs' + - sphinxcontrib-github-alt ; extra == 'docs' + - sphinxcontrib-spelling ; extra == 'docs' + - orjson ; extra == 'orjson' + - anyio ; extra == 'test' + - coverage ; extra == 'test' + - ipykernel>=6.14 ; extra == 'test' + - msgpack ; extra == 'test' + - mypy ; platform_python_implementation != 'PyPy' and extra == 'test' + - paramiko ; sys_platform == 'win32' and extra == 'test' + - pre-commit ; extra == 'test' + - pytest ; extra == 'test' + - pytest-cov ; extra == 'test' + - pytest-jupyter[client]>=0.6.2 ; extra == 'test' + - pytest-timeout ; extra == 'test' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/ca/77/71d78d58f15c22db16328a476426f7ac4a60d3a5a7ba3b9627ee2f7903d4/jupyter_console-6.6.3-py3-none-any.whl + name: jupyter-console + version: 6.6.3 + sha256: 309d33409fcc92ffdad25f0bcdf9a4a9daa61b6f341177570fdac03de5352485 + requires_dist: + - ipykernel>=6.14 - ipython - - jupyter_client >=7.0.0 - - jupyter_core >=4.12,!=5.0.* - - prompt_toolkit >=3.0.30 + - jupyter-client>=7.0.0 + - jupyter-core>=4.12,!=5.0.* + - prompt-toolkit>=3.0.30 - pygments - - python >=3.9 - - pyzmq >=17 - - traitlets >=5.4 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/jupyter-console?source=hash-mapping - size: 26874 - timestamp: 1733818130068 -- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.7.2-pyh31011fe_1.conda - sha256: 732b1e8536bc22a5a174baa79842d79db2f4956d90293dd82dc1b3f6099bcccd - md5: 0a2980dada0dd7fd0998f0342308b1b1 - depends: - - __unix - - platformdirs >=2.5 - - python >=3.8 - - traitlets >=5.3 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/jupyter-core?source=hash-mapping - size: 57671 - timestamp: 1727163547058 + - pyzmq>=17 + - traitlets>=5.4 + - flaky ; extra == 'test' + - pexpect ; extra == 'test' + - pytest ; extra == 'test' + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl + name: jupyter-core + version: 5.9.1 + sha256: ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407 + requires_dist: + - platformdirs>=2.5 + - traitlets>=5.3 + - intersphinx-registry ; extra == 'docs' + - myst-parser ; extra == 'docs' + - pydata-sphinx-theme ; extra == 'docs' + - sphinx-autodoc-typehints ; extra == 'docs' + - sphinxcontrib-spelling ; extra == 'docs' + - traitlets ; extra == 'docs' + - ipykernel ; extra == 'test' + - pre-commit ; extra == 'test' + - pytest-cov ; extra == 'test' + - pytest-timeout ; extra == 'test' + - pytest<9 ; extra == 'test' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/23/e8/9d61dcbd1dce8ef418f06befd4ac084b4720429c26b0b1222bc218685eff/jupyter_lsp-2.3.1-py3-none-any.whl + name: jupyter-lsp + version: 2.3.1 + sha256: 71b954d834e85ff3096400554f2eefaf7fe37053036f9a782b0f7c5e42dadb81 + requires_dist: + - jupyter-server>=1.1.2 + - importlib-metadata>=4.8.3 ; python_full_version < '3.10' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/e2/50/ecf4f70d65bdb7519b28a33d1b2fee8a4b4ba1ae1a92f15d97e877c5de21/jupyter_server-2.18.2-py3-none-any.whl + name: jupyter-server + version: 2.18.2 + sha256: fa5e46539ded65791838035a2b6001f13e54d5f64b8b3752eb1e91fdd641a5b8 + requires_dist: + - anyio>=3.1.0 + - argon2-cffi>=21.1 + - jinja2>=3.0.3 + - jupyter-client>=7.4.4 + - jupyter-core>=4.12,!=5.0.* + - jupyter-events>=0.11.0 + - jupyter-server-terminals>=0.4.4 + - nbconvert>=6.4.4 + - nbformat>=5.3.0 + - overrides>=5.0 ; python_full_version < '3.12' + - packaging>=22.0 + - prometheus-client>=0.9 + - pywinpty>=2.0.1 ; os_name == 'nt' + - pyzmq>=24 + - send2trash>=1.8.2 + - terminado>=0.8.3 + - tornado>=6.2.0 + - traitlets>=5.6.0 + - websocket-client>=1.7 + - ipykernel ; extra == 'docs' + - jinja2 ; extra == 'docs' + - jupyter-client ; extra == 'docs' + - myst-parser ; extra == 'docs' + - nbformat ; extra == 'docs' + - prometheus-client ; extra == 'docs' + - pydata-sphinx-theme ; extra == 'docs' + - send2trash ; extra == 'docs' + - sphinx-autodoc-typehints ; extra == 'docs' + - sphinx<9.0 ; extra == 'docs' + - sphinxcontrib-github-alt ; extra == 'docs' + - sphinxcontrib-openapi>=0.8.0 ; extra == 'docs' + - sphinxcontrib-spelling ; extra == 'docs' + - sphinxemoji ; extra == 'docs' + - tornado ; extra == 'docs' + - typing-extensions ; extra == 'docs' + - flaky ; extra == 'test' + - ipykernel ; extra == 'test' + - pre-commit ; extra == 'test' + - pytest-console-scripts ; extra == 'test' + - pytest-jupyter[server]>=0.7 ; extra == 'test' + - pytest-timeout ; extra == 'test' + - pytest>=7.0,<9 ; extra == 'test' + - requests ; extra == 'test' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/d1/2d/6674563f71c6320841fc300911a55143925112a72a883e2ca71fba4c618d/jupyter_server_terminals-0.5.4-py3-none-any.whl + name: jupyter-server-terminals + version: 0.5.4 + sha256: 55be353fc74a80bc7f3b20e6be50a55a61cd525626f578dcb66a5708e2007d14 + requires_dist: + - pywinpty>=2.0.3 ; os_name == 'nt' + - terminado>=0.8.3 + - jinja2 ; extra == 'docs' + - jupyter-server ; extra == 'docs' + - mistune<4.0 ; extra == 'docs' + - myst-parser ; extra == 'docs' + - nbformat ; extra == 'docs' + - packaging ; extra == 'docs' + - pydata-sphinx-theme ; extra == 'docs' + - sphinxcontrib-github-alt ; extra == 'docs' + - sphinxcontrib-openapi ; extra == 'docs' + - sphinxcontrib-spelling ; extra == 'docs' + - sphinxemoji ; extra == 'docs' + - tornado ; extra == 'docs' + - jupyter-server>=2.0.0 ; extra == 'test' + - pytest-jupyter[server]>=0.5.3 ; extra == 'test' + - pytest-timeout ; extra == 'test' + - pytest>=7.0 ; extra == 'test' + requires_python: '>=3.8' - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.0-pyh29332c3_0.conda sha256: 37e6ac3ccf7afcc730c3b93cb91a13b9ae827fd306f35dd28f958a74a14878b5 md5: f56000b36f09ab7533877e695e4e8cb0 @@ -2650,47 +2511,6 @@ packages: - pkg:pypi/jupyter-events?source=compressed-mapping size: 23647 timestamp: 1738765986736 -- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.15.0-pyhd8ed1ab_0.conda - sha256: be5f9774065d94c4a988f53812b83b67618bec33fcaaa005a98067d506613f8a - md5: 6ba8c206b5c6f52b82435056cf74ee46 - depends: - - anyio >=3.1.0 - - argon2-cffi >=21.1 - - jinja2 >=3.0.3 - - jupyter_client >=7.4.4 - - jupyter_core >=4.12,!=5.0.* - - jupyter_events >=0.11.0 - - jupyter_server_terminals >=0.4.4 - - nbconvert-core >=6.4.4 - - nbformat >=5.3.0 - - overrides >=5.0 - - packaging >=22.0 - - prometheus_client >=0.9 - - python >=3.9 - - pyzmq >=24 - - send2trash >=1.8.2 - - terminado >=0.8.3 - - tornado >=6.2.0 - - traitlets >=5.6.0 - - websocket-client >=1.7 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/jupyter-server?source=hash-mapping - size: 327747 - timestamp: 1734702771032 -- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.3-pyhd8ed1ab_1.conda - sha256: 0890fc79422191bc29edf17d7b42cff44ba254aa225d31eb30819f8772b775b8 - md5: 2d983ff1b82a1ccb6f2e9d8784bdd6bd - depends: - - python >=3.9 - - terminado >=0.8.3 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/jupyter-server-terminals?source=hash-mapping - size: 19711 - timestamp: 1733428049134 - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterhub-5.1.0-pyh31011fe_0.conda sha256: 61bd3d20c154d26902b824679b5c499a1047ff876d79fe87141d8aae296a24d8 md5: 076d727bb86cefca6653918a658d3836 @@ -2769,99 +2589,145 @@ packages: - pkg:pypi/jupyterhub-kubespawner?source=hash-mapping size: 59844 timestamp: 1700744061778 -- conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.3.6-pyhd8ed1ab_0.conda - sha256: cf10c9b4158c4ef2796fde546f2bbe45f43c1402a0c2a175939ebbb308846ada - md5: 8b91a10c966aa65b9ad1a2702e6ef121 - depends: - - async-lru >=1.0.0 - - httpx >=0.25.0 - - importlib-metadata >=4.8.3 - - ipykernel >=6.5.0 - - jinja2 >=3.0.3 - - jupyter-lsp >=2.0.0 - - jupyter_core - - jupyter_server >=2.4.0,<3 - - jupyterlab_server >=2.27.1,<3 - - notebook-shim >=0.2 +- pypi: https://files.pythonhosted.org/packages/3d/aa/537b8f7d80e799af19af35fb3ddfc970b951088a13c57dd9387dcfbb7f61/jupyterlab-4.5.7-py3-none-any.whl + name: jupyterlab + version: 4.5.7 + sha256: fba4cb0e2c44a52859669d8c98b45de029d5e515f8407bf8534d2a8fc5f0964d + requires_dist: + - async-lru>=1.0.0 + - httpx>=0.25.0,<1 + - importlib-metadata>=4.8.3 ; python_full_version < '3.10' + - ipykernel>=6.5.0,!=6.30.0 + - jinja2>=3.0.3 + - jupyter-core + - jupyter-lsp>=2.0.0 + - jupyter-server>=2.4.0,<3 + - jupyterlab-server>=2.28.0,<3 + - notebook-shim>=0.2 - packaging - - python >=3.9 - - setuptools >=40.8.0 - - tomli >=1.2.2 - - tornado >=6.2.0 + - setuptools>=41.1.0 + - tomli>=1.2.2 ; python_full_version < '3.11' + - tornado>=6.2.0 - traitlets - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/jupyterlab?source=compressed-mapping - size: 7641308 - timestamp: 1741964212957 -- conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda - sha256: dc24b900742fdaf1e077d9a3458fd865711de80bca95fe3c6d46610c532c6ef0 - md5: fd312693df06da3578383232528c468d - depends: - - pygments >=2.4.1,<3 - - python >=3.9 - constrains: - - jupyterlab >=4.0.8,<5.0.0 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/jupyterlab-pygments?source=hash-mapping - size: 18711 - timestamp: 1733328194037 -- conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.27.3-pyhd8ed1ab_1.conda - sha256: d03d0b7e23fa56d322993bc9786b3a43b88ccc26e58b77c756619a921ab30e86 - md5: 9dc4b2b0f41f0de41d27f3293e319357 - depends: - - babel >=2.10 - - importlib-metadata >=4.8.3 - - jinja2 >=3.0.3 - - json5 >=0.9.0 - - jsonschema >=4.18 - - jupyter_server >=1.21,<3 - - packaging >=21.3 - - python >=3.9 - - requests >=2.31 - constrains: - - openapi-core >=0.18.0,<0.19.0 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/jupyterlab-server?source=hash-mapping - size: 49449 - timestamp: 1733599666357 -- conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_widgets-3.0.13-pyhd8ed1ab_1.conda - sha256: 206489e417408d2ffc2a7b245008b4735a8beb59df6c9109d4f77e7bc5969d5d - md5: b26e487434032d7f486277beb0cead3a - depends: - - python >=3.9 - constrains: - - jupyterlab >=3,<5 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/jupyterlab-widgets?source=hash-mapping - size: 186358 - timestamp: 1733428156991 -- conda: https://conda.anaconda.org/conda-forge/noarch/keyring-25.6.0-pyha804496_0.conda - sha256: b6f57c17cf098022c32fe64e85e9615d427a611c48a5947cdfc357490210a124 - md5: cdd58ab99c214b55d56099108a914282 - depends: - - __linux - - importlib-metadata >=4.11.4 - - importlib_resources - - jaraco.classes - - jaraco.context - - jaraco.functools - - jeepney >=0.4.2 - - python >=3.9 - - secretstorage >=3.2 - license: MIT - license_family: MIT - purls: - - pkg:pypi/keyring?source=hash-mapping - size: 36985 - timestamp: 1735210286595 + - build ; extra == 'dev' + - bump2version ; extra == 'dev' + - coverage ; extra == 'dev' + - hatch ; extra == 'dev' + - pre-commit ; extra == 'dev' + - pytest-cov ; extra == 'dev' + - ruff==0.11.12 ; extra == 'dev' + - jsx-lexer ; extra == 'docs' + - myst-parser ; extra == 'docs' + - pydata-sphinx-theme>=0.13.0 ; extra == 'docs' + - pytest ; extra == 'docs' + - pytest-check-links ; extra == 'docs' + - pytest-jupyter ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - sphinx>=1.8,<8.2.0 ; extra == 'docs' + - altair==6.0.0 ; extra == 'docs-screenshots' + - ipython==8.16.1 ; extra == 'docs-screenshots' + - ipywidgets==8.1.5 ; extra == 'docs-screenshots' + - jupyterlab-geojson==3.4.0 ; extra == 'docs-screenshots' + - jupyterlab-language-pack-zh-cn==4.3.post1 ; extra == 'docs-screenshots' + - matplotlib==3.10.0 ; extra == 'docs-screenshots' + - nbconvert>=7.0.0 ; extra == 'docs-screenshots' + - pandas==2.2.3 ; extra == 'docs-screenshots' + - scipy==1.15.1 ; extra == 'docs-screenshots' + - coverage ; extra == 'test' + - pytest-check-links>=0.7 ; extra == 'test' + - pytest-console-scripts ; extra == 'test' + - pytest-cov ; extra == 'test' + - pytest-jupyter>=0.5.3 ; extra == 'test' + - pytest-timeout ; extra == 'test' + - pytest-tornasync ; extra == 'test' + - pytest>=7.0 ; extra == 'test' + - requests ; extra == 'test' + - requests-cache ; extra == 'test' + - virtualenv ; extra == 'test' + - copier>=9,<10 ; extra == 'upgrade-extension' + - jinja2-time<0.3 ; extra == 'upgrade-extension' + - pydantic<3.0 ; extra == 'upgrade-extension' + - pyyaml-include<3.0 ; extra == 'upgrade-extension' + - tomli-w<2.0 ; extra == 'upgrade-extension' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl + name: jupyterlab-pygments + version: 0.3.0 + sha256: 841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl + name: jupyterlab-server + version: 2.28.0 + sha256: e4355b148fdcf34d312bbbc80f22467d6d20460e8b8736bf235577dd18506968 + requires_dist: + - babel>=2.10 + - importlib-metadata>=4.8.3 ; python_full_version < '3.10' + - jinja2>=3.0.3 + - json5>=0.9.0 + - jsonschema>=4.18.0 + - jupyter-server>=1.21,<3 + - packaging>=21.3 + - requests>=2.31 + - autodoc-traits ; extra == 'docs' + - jinja2<3.2.0 ; extra == 'docs' + - mistune<4 ; extra == 'docs' + - myst-parser ; extra == 'docs' + - pydata-sphinx-theme ; extra == 'docs' + - sphinx ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - sphinxcontrib-openapi>0.8 ; extra == 'docs' + - openapi-core~=0.18.0 ; extra == 'openapi' + - ruamel-yaml ; extra == 'openapi' + - hatch ; extra == 'test' + - ipykernel ; extra == 'test' + - openapi-core~=0.18.0 ; extra == 'test' + - openapi-spec-validator>=0.6.0,<0.8.0 ; extra == 'test' + - pytest-console-scripts ; extra == 'test' + - pytest-cov ; extra == 'test' + - pytest-jupyter[server]>=0.6.2 ; extra == 'test' + - pytest-timeout ; extra == 'test' + - pytest>=7.0,<8 ; extra == 'test' + - requests-mock ; extra == 'test' + - ruamel-yaml ; extra == 'test' + - sphinxcontrib-spelling ; extra == 'test' + - strict-rfc3339 ; extra == 'test' + - werkzeug ; extra == 'test' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl + name: jupyterlab-widgets + version: 3.0.16 + sha256: 45fa36d9c6422cf2559198e4db481aa243c7a32d9926b500781c830c80f7ecf8 + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl + name: keyring + version: 25.7.0 + sha256: be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f + requires_dist: + - pywin32-ctypes>=0.2.0 ; sys_platform == 'win32' + - secretstorage>=3.2 ; sys_platform == 'linux' + - jeepney>=0.4.2 ; sys_platform == 'linux' + - importlib-metadata>=4.11.4 ; python_full_version < '3.12' + - jaraco-classes + - jaraco-functools + - jaraco-context + - pytest>=6,!=8.1.* ; extra == 'test' + - pyfakefs ; extra == 'test' + - sphinx>=3.5 ; extra == 'doc' + - jaraco-packaging>=9.3 ; extra == 'doc' + - rst-linker>=1.9 ; extra == 'doc' + - furo ; extra == 'doc' + - sphinx-lint ; extra == 'doc' + - jaraco-tidelift>=1.4 ; extra == 'doc' + - pytest-checkdocs>=2.4 ; extra == 'check' + - pytest-ruff>=0.2.1 ; sys_platform != 'cygwin' and extra == 'check' + - pytest-cov ; extra == 'cover' + - pytest-enabler>=3.4 ; extra == 'enabler' + - pytest-mypy>=1.0.1 ; extra == 'type' + - pygobject-stubs ; extra == 'type' + - shtab ; extra == 'type' + - types-pywin32 ; extra == 'type' + - shtab>=1.1.0 ; extra == 'completion' + requires_python: '>=3.9' - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.1-h166bdaf_0.tar.bz2 sha256: 150c05a6e538610ca7c43beb3a40d65c90537497a4f6a5f4d15ec0451b6f5ebb md5: 30186d27e2c9fa62b45fb1476b7200e3 @@ -2928,31 +2794,6 @@ packages: - pkg:pypi/kubernetes-asyncio?source=hash-mapping size: 483321 timestamp: 1705631401087 -- conda: https://conda.anaconda.org/conda-forge/linux-64/lcms2-2.17-h717163a_0.conda - sha256: d6a61830a354da022eae93fa896d0991385a875c6bba53c82263a289deda9db8 - md5: 000e85703f0fd9594c81710dd5066471 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - libjpeg-turbo >=3.0.0,<4.0a0 - - libtiff >=4.7.0,<4.8.0a0 - license: MIT - license_family: MIT - purls: [] - size: 248046 - timestamp: 1739160907615 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/lcms2-2.17-hc88f144_0.conda - sha256: 47cf6a4780dc41caa9bc95f020eed485b07010c9ccc85e9ef44b538fedb5341d - md5: b87b1abd2542cf65a00ad2e2461a3083 - depends: - - libgcc >=13 - - libjpeg-turbo >=3.0.0,<4.0a0 - - libtiff >=4.7.0,<4.8.0a0 - license: MIT - license_family: MIT - purls: [] - size: 287007 - timestamp: 1739161069194 - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.43-h712a8e2_4.conda sha256: db73f38155d901a610b2320525b9dd3b31e4949215c870685fd92ea61b5ce472 md5: 01f8d123c96816249efd255a31ad7712 @@ -2975,173 +2816,10 @@ packages: purls: [] size: 699058 timestamp: 1740155620594 -- conda: https://conda.anaconda.org/conda-forge/linux-64/lerc-4.0.0-h27087fc_0.tar.bz2 - sha256: cb55f36dcd898203927133280ae1dc643368af041a48bcf7c026acb7c47b0c12 - md5: 76bbff344f0134279f225174e9064c8f - depends: - - libgcc-ng >=12 - - libstdcxx-ng >=12 - license: Apache-2.0 - license_family: Apache - purls: [] - size: 281798 - timestamp: 1657977462600 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/lerc-4.0.0-h4de3ea5_0.tar.bz2 - sha256: 2d09ef9b7796d83364957e420b41c32d94e628c3f0520b61c332518a7b5cd586 - md5: 1a0ffc65e03ce81559dbcb0695ad1476 - depends: - - libgcc-ng >=12 - - libstdcxx-ng >=12 - license: Apache-2.0 - license_family: Apache - purls: [] - size: 262096 - timestamp: 1657978241894 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libarchive-3.7.7-h4585015_3.conda - sha256: 2466803e26ae9dbd2263de3a102b572b741c056549875c04b6ec10830bd5d338 - md5: a28808eae584c7f519943719b2a2b386 - depends: - - __glibc >=2.17,<3.0.a0 - - bzip2 >=1.0.8,<2.0a0 - - libgcc >=13 - - liblzma >=5.6.3,<6.0a0 - - libxml2 >=2.13.5,<3.0a0 - - libzlib >=1.3.1,<2.0a0 - - lz4-c >=1.10.0,<1.11.0a0 - - lzo >=2.10,<3.0a0 - - openssl >=3.4.0,<4.0a0 - - zstd >=1.5.6,<1.6.0a0 - license: BSD-2-Clause - license_family: BSD - purls: [] - size: 878021 - timestamp: 1734020918345 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libarchive-3.7.7-h6223a6c_3.conda - sha256: d8f6e3cf775f7aa10e767ba8daea2ae587f85cd782ea9d63d78bf990fcd47a46 - md5: e5ab5ecbdc352a5decae39c30c248fb8 - depends: - - bzip2 >=1.0.8,<2.0a0 - - libgcc >=13 - - liblzma >=5.6.3,<6.0a0 - - libxml2 >=2.13.5,<3.0a0 - - libzlib >=1.3.1,<2.0a0 - - lz4-c >=1.10.0,<1.11.0a0 - - lzo >=2.10,<3.0a0 - - openssl >=3.4.0,<4.0a0 - - zstd >=1.5.6,<1.6.0a0 - license: BSD-2-Clause - license_family: BSD - purls: [] - size: 991078 - timestamp: 1734020964844 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libasprintf-0.25.1-h3f43e3d_1.conda - sha256: cb728a2a95557bb6a5184be2b8be83a6f2083000d0c7eff4ad5bbe5792133541 - md5: 3b0d184bc9404516d418d4509e418bdc - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - - libstdcxx >=14 - license: LGPL-2.1-or-later - purls: [] - size: 53582 - timestamp: 1753342901341 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libasprintf-0.25.1-h5e0f5ae_0.conda - sha256: 146be90c237cf3d8399e44afe5f5d21ef9a15a7983ccea90e72d4ae0362f9b28 - md5: 1c5813f6be57f087b6659593248daf00 - depends: - - libgcc >=13 - - libstdcxx >=13 - license: LGPL-2.1-or-later - purls: [] - size: 53434 - timestamp: 1751557548397 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libasprintf-devel-0.25.1-h3f43e3d_1.conda - sha256: 2fc95060efc3d76547b7872875af0b7212d4b1407165be11c5f830aeeb57fc3a - md5: fd9cf4a11d07f0ef3e44fc061611b1ed - depends: - - __glibc >=2.17,<3.0.a0 - - libasprintf 0.25.1 h3f43e3d_1 - - libgcc >=14 - license: LGPL-2.1-or-later - purls: [] - size: 34734 - timestamp: 1753342921605 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libasprintf-devel-0.25.1-h5e0f5ae_0.conda - sha256: cc2bb8ca349ba4dd4af7971a3dba006bc8643353acd9757b4d645a817ec0f899 - md5: 5df92d925fba917586f3ca31c96d8e6d - depends: - - libasprintf 0.25.1 h5e0f5ae_0 - - libgcc >=13 - license: LGPL-2.1-or-later - purls: [] - size: 34824 - timestamp: 1751557562978 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-31_h59b9bed_openblas.conda - build_number: 31 - sha256: 9839fc4ac0cbb0aa3b9eea520adfb57311838959222654804e58f6f2d1771db5 - md5: 728dbebd0f7a20337218beacffd37916 - depends: - - libopenblas >=0.3.29,<0.3.30.0a0 - - libopenblas >=0.3.29,<1.0a0 - constrains: - - liblapacke =3.9.0=31*_openblas - - liblapack =3.9.0=31*_openblas - - blas =2.131=openblas - - mkl <2025 - - libcblas =3.9.0=31*_openblas - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 16859 - timestamp: 1740087969120 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libblas-3.9.0-31_h1a9f1db_openblas.conda - build_number: 31 - sha256: 67c9c81dd0444ecc712124034d9f74186ca82fd770b3df46b1a68564461c6a1a - md5: 48bd5bf15ccf3e409840be9caafc0ad5 - depends: - - libopenblas >=0.3.29,<0.3.30.0a0 - - libopenblas >=0.3.29,<1.0a0 - constrains: - - liblapack =3.9.0=31*_openblas - - blas =2.131=openblas - - libcblas =3.9.0=31*_openblas - - mkl <2025 - - liblapacke =3.9.0=31*_openblas - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 16915 - timestamp: 1740087911042 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-31_he106b2a_openblas.conda - build_number: 31 - sha256: ede8545011f5b208b151fe3e883eb4e31d495ab925ab7b9ce394edca846e0c0d - md5: abb32c727da370c481a1c206f5159ce9 - depends: - - libblas 3.9.0 31_h59b9bed_openblas - constrains: - - liblapacke =3.9.0=31*_openblas - - liblapack =3.9.0=31*_openblas - - blas =2.131=openblas - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 16796 - timestamp: 1740087984429 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libcblas-3.9.0-31_hab92f65_openblas.conda - build_number: 31 - sha256: f0457a1d2982f0a28bfbadaa02621677c324e88f7c8198c24fb3e3214c468dba - md5: 6b81dbae56a519f1ec2f25e0ee2f4334 - depends: - - libblas 3.9.0 31_h1a9f1db_openblas - constrains: - - liblapack =3.9.0=31*_openblas - - blas =2.131=openblas - - liblapacke =3.9.0=31*_openblas - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 16824 - timestamp: 1740087917500 +- pypi: https://files.pythonhosted.org/packages/88/3f/ff00c588ebd7eae46a9d6223389f5ae28a3af4b6d975c0f2a6d86b1342b9/libarchive_c-5.3-py3-none-any.whl + name: libarchive-c + version: '5.3' + sha256: 651550a6ec39266b78f81414140a1e04776c935e72dfc70f1d7c8e0a3672ffba - conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.18.0-h4e3cde8_0.conda sha256: 5454709d9fb6e9c3dd6423bc284fa7835a7823bfa8323f6e8786cdd555101fab md5: 0a5563efed19ca4461cf927419b6eb73 @@ -3175,27 +2853,6 @@ packages: purls: [] size: 482649 timestamp: 1767821674919 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.23-h4ddbbb0_0.conda - sha256: 511d801626d02f4247a04fff957cc6e9ec4cc7e8622bd9acd076bcdc5de5fe66 - md5: 8dfae1d2e74767e9ce36d5fa0d8605db - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - license: MIT - license_family: MIT - purls: [] - size: 72255 - timestamp: 1734373823254 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libdeflate-1.23-h5e3c512_0.conda - sha256: 959419d87cd2b789a9055db95704c614f31aeb70bef7949fa2f734122a3a2863 - md5: 7e7ca2607b11b180120cefc2354fc0cb - depends: - - libgcc >=13 - license: MIT - license_family: MIT - purls: [] - size: 69862 - timestamp: 1734373858306 - conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda sha256: d789471216e7aba3c184cd054ed61ce3f6dac6f87a50ec69291b9297f8c18724 md5: c277e0a4d549b03ac1e9d6cbbe3d017b @@ -3334,135 +2991,6 @@ packages: purls: [] size: 53622 timestamp: 1740241074834 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libgettextpo-0.25.1-h3f43e3d_1.conda - sha256: 50a9e9815cf3f5bce1b8c5161c0899cc5b6c6052d6d73a4c27f749119e607100 - md5: 2f4de899028319b27eb7a4023be5dfd2 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - - libiconv >=1.18,<2.0a0 - license: GPL-3.0-or-later - license_family: GPL - purls: [] - size: 188293 - timestamp: 1753342911214 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgettextpo-0.25.1-h5ad3122_0.conda - sha256: c8e5590166f4931a3ab01e444632f326e1bb00058c98078eb46b6e8968f1b1e9 - md5: ad7b109fbbff1407b1a7eeaa60d7086a - depends: - - libgcc >=13 - license: GPL-3.0-or-later - license_family: GPL - purls: [] - size: 225352 - timestamp: 1751557555903 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libgettextpo-devel-0.25.1-h3f43e3d_1.conda - sha256: c7ea10326fd450a2a21955987db09dde78c99956a91f6f05386756a7bfe7cc04 - md5: 3f7a43b3160ec0345c9535a9f0d7908e - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - - libgettextpo 0.25.1 h3f43e3d_1 - - libiconv >=1.18,<2.0a0 - license: GPL-3.0-or-later - license_family: GPL - purls: [] - size: 37407 - timestamp: 1753342931100 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgettextpo-devel-0.25.1-h5ad3122_0.conda - sha256: a26e1982d062daba5bdd3a90a2ef77b323803d21d27cf4e941135f07037d6649 - md5: 0d9d56bac6e4249da2bede0588ae1c1b - depends: - - libgcc >=13 - - libgettextpo 0.25.1 h5ad3122_0 - license: GPL-3.0-or-later - license_family: GPL - purls: [] - size: 37460 - timestamp: 1751557569909 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran-14.2.0-h69a702a_2.conda - sha256: e05263e8960da03c341650f2a3ffa4ccae4e111cb198e8933a2908125459e5a6 - md5: fb54c4ea68b460c278d26eea89cfbcc3 - depends: - - libgfortran5 14.2.0 hf1ad2bd_2 - constrains: - - libgfortran-ng ==14.2.0=*_2 - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL - purls: [] - size: 53733 - timestamp: 1740240690977 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgfortran-14.2.0-he9431aa_2.conda - sha256: 996d3c0505301901a7ab23b5e7daad21635d1c065240bb0f4faf7e4f75d7f49d - md5: d8b9d9dc0c8cd97d375b48e55947ba70 - depends: - - libgfortran5 14.2.0 hb6113d0_2 - constrains: - - libgfortran-ng ==14.2.0=*_2 - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL - purls: [] - size: 53611 - timestamp: 1740241100147 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-14.2.0-hf1ad2bd_2.conda - sha256: c17b7cf3073a1f4e1f34d50872934fa326346e104d3c445abc1e62481ad6085c - md5: 556a4fdfac7287d349b8f09aba899693 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14.2.0 - constrains: - - libgfortran 14.2.0 - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL - purls: [] - size: 1461978 - timestamp: 1740240671964 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgfortran5-14.2.0-hb6113d0_2.conda - sha256: 7b9e1d3666a00e5a52e5d43c003bd1c73ab472804be513c070aaedca9c4c2a9a - md5: cd754566661513808ef2408c4ab99a2f - depends: - - libgcc >=14.2.0 - constrains: - - libgfortran 14.2.0 - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL - purls: [] - size: 1100765 - timestamp: 1740241083241 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libglib-2.72.1-h2d90d5f_0.tar.bz2 - sha256: 2ec01b1fbd21f9ec4a0a723a7dbe0c43db2f7dde88eb95586d63ea7f4e40193f - md5: ebeadbb5fbc44052eeb6f96a2136e3c2 - depends: - - gettext >=0.19.8.1,<1.0a0 - - libffi >=3.4.2,<3.5.0a0 - - libgcc-ng >=12 - - libiconv >=1.16,<2.0.0a0 - - libstdcxx-ng >=12 - - libzlib >=1.2.12,<2.0.0a0 - - pcre >=8.45,<9.0a0 - constrains: - - glib 2.72.1 *_0 - license: LGPL-2.1-or-later - purls: [] - size: 3243396 - timestamp: 1657548239702 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libglib-2.72.1-hd4f7528_0.tar.bz2 - sha256: 7f08380e6360b27d8f24932504ca8f7a55f8983a5014b731a185b4935bbcd4c0 - md5: 1212b8e4725cbbee98021b11cbfba4fe - depends: - - gettext >=0.19.8.1,<1.0a0 - - libffi >=3.4.2,<3.5.0a0 - - libgcc-ng >=12 - - libiconv >=1.16,<2.0.0a0 - - libstdcxx-ng >=12 - - libzlib >=1.2.12,<2.0.0a0 - - pcre >=8.45,<9.0a0 - constrains: - - glib 2.72.1 *_0 - license: LGPL-2.1-or-later - purls: [] - size: 3290978 - timestamp: 1657548057761 - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-14.2.0-h767d61c_2.conda sha256: 1a3130e0b9267e781b89399580f3163632d59fe5b0142900d63052ab1a53490e md5: 06d02030237f4d5b3d9a7e7d348fe3c6 @@ -3523,58 +3051,6 @@ packages: purls: [] size: 147165 timestamp: 1760387531719 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.0.0-hd590300_1.conda - sha256: b954e09b7e49c2f2433d6f3bb73868eda5e378278b0f8c1dd10a7ef090e14f2f - md5: ea25936bb4080d843790b586850f82b8 - depends: - - libgcc-ng >=12 - constrains: - - jpeg <0.0.0a - license: IJG AND BSD-3-Clause AND Zlib - purls: [] - size: 618575 - timestamp: 1694474974816 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libjpeg-turbo-3.0.0-h31becfc_1.conda - sha256: 675bc1f2a8581cd34a86c412663ec29c5f90c1d9f8d11866aa1ade5cdbdf8429 - md5: ed24e702928be089d9ba3f05618515c6 - depends: - - libgcc-ng >=12 - constrains: - - jpeg <0.0.0a - license: IJG AND BSD-3-Clause AND Zlib - purls: [] - size: 647126 - timestamp: 1694475003570 -- conda: https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-31_h7ac8fdf_openblas.conda - build_number: 31 - sha256: f583661921456e798aba10972a8abbd9d33571c655c1f66eff450edc9cbefcf3 - md5: 452b98eafe050ecff932f0ec832dd03f - depends: - - libblas 3.9.0 31_h59b9bed_openblas - constrains: - - libcblas =3.9.0=31*_openblas - - liblapacke =3.9.0=31*_openblas - - blas =2.131=openblas - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 16790 - timestamp: 1740087997375 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/liblapack-3.9.0-31_h411afd4_openblas.conda - build_number: 31 - sha256: 27613828ff6fb258b2e58802617df549f00089660ea8ab6c55c68f042c570162 - md5: 41dbff5eb805a75c120a7b7a1c744dc2 - depends: - - libblas 3.9.0 31_h1a9f1db_openblas - constrains: - - blas =2.131=openblas - - libcblas =3.9.0=31*_openblas - - liblapacke =3.9.0=31*_openblas - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 16845 - timestamp: 1740087923843 - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.6.4-hb9d3cd8_0.conda sha256: cad52e10319ca4585bc37f0bc7cce99ec7c15dc9168e42ccb96b741b0a27db3f md5: 42d5b6a0f30d3c10cd88cb8584fda1cb @@ -3629,92 +3105,24 @@ packages: timestamp: 1756835019535 - conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hd590300_0.conda sha256: 26d77a3bb4dceeedc2a41bd688564fe71bf2d149fdcf117049970bc02ff1add6 - md5: 30fd6e37fe21f86f4bd26d6ee73eeec7 - depends: - - libgcc-ng >=12 - license: LGPL-2.1-only - license_family: GPL - purls: [] - size: 33408 - timestamp: 1697359010159 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libnsl-2.0.1-h31becfc_0.conda - sha256: fd18c2b75d7411096428d36a70b36b1a17e31f7b8956b6905d145792d49e97f8 - md5: c14f32510f694e3185704d89967ec422 - depends: - - libgcc-ng >=12 - license: LGPL-2.1-only - license_family: GPL - purls: [] - size: 34501 - timestamp: 1697358973269 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.29-pthreads_h94d23a6_0.conda - sha256: cc5389ea254f111ef17a53df75e8e5209ef2ea6117e3f8aced88b5a8e51f11c4 - md5: 0a4d0252248ef9a0f88f2ba8b8a08e12 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - - libgfortran - - libgfortran5 >=14.2.0 - constrains: - - openblas >=0.3.29,<0.3.30.0a0 - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 5919288 - timestamp: 1739825731827 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libopenblas-0.3.29-pthreads_h9d3fd7e_0.conda - sha256: 3a2ccf4c9098cd18a636e9b7fff947fdeb4962bcfb75c9d6fd80b8c50caf6a3c - md5: a99e2bfcb1ad6362544c71281eb617e9 - depends: - - libgcc >=14 - - libgfortran - - libgfortran5 >=14.2.0 - constrains: - - openblas >=0.3.29,<0.3.30.0a0 - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 4801657 - timestamp: 1739825308974 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.47-h943b412_0.conda - sha256: 23367d71da58c9a61c8cbd963fcffb92768d4ae5ffbef9a47cdf1f54f98c5c36 - md5: 55199e2ae2c3651f6f9b2a447b47bdc9 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - libzlib >=1.3.1,<2.0a0 - license: zlib-acknowledgement - purls: [] - size: 288701 - timestamp: 1739952993639 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libpng-1.6.47-hec79eb8_0.conda - sha256: 3861a65106a5f876eff3fc19042c3edb528216114b9f8e64b37aebf003deda11 - md5: c4b1ba0d7cef5002759d2f156722feee - depends: - - libgcc >=13 - - libzlib >=1.3.1,<2.0a0 - license: zlib-acknowledgement - purls: [] - size: 291536 - timestamp: 1739957375872 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libsodium-1.0.20-h4ab18f5_0.conda - sha256: 0105bd108f19ea8e6a78d2d994a6d4a8db16d19a41212070d2d1d48a63c34161 - md5: a587892d3c13b6621a6091be690dbca2 + md5: 30fd6e37fe21f86f4bd26d6ee73eeec7 depends: - libgcc-ng >=12 - license: ISC + license: LGPL-2.1-only + license_family: GPL purls: [] - size: 205978 - timestamp: 1716828628198 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libsodium-1.0.20-h68df207_0.conda - sha256: 448df5ea3c5cf1af785aad46858d7a5be0522f4234a4dc9bb764f4d11ff3b981 - md5: 2e4a8f23bebdcb85ca8e5a0fbe75666a + size: 33408 + timestamp: 1697359010159 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libnsl-2.0.1-h31becfc_0.conda + sha256: fd18c2b75d7411096428d36a70b36b1a17e31f7b8956b6905d145792d49e97f8 + md5: c14f32510f694e3185704d89967ec422 depends: - libgcc-ng >=12 - license: ISC + license: LGPL-2.1-only + license_family: GPL purls: [] - size: 177394 - timestamp: 1716828514515 + size: 34501 + timestamp: 1697358973269 - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.49.1-hee588c1_2.conda sha256: a086289bf75c33adc1daed3f1422024504ffb5c3c8b3285c49f025c29708ed16 md5: 962d6ac93c30b1dfc54c9cccafd1003e @@ -3802,41 +3210,6 @@ packages: purls: [] size: 53715 timestamp: 1740241126343 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.7.0-hd9ff511_3.conda - sha256: b224e16b88d76ea95e4af56e2bc638c603bd26a770b98d117d04541d3aafa002 - md5: 0ea6510969e1296cc19966fad481f6de - depends: - - __glibc >=2.17,<3.0.a0 - - lerc >=4.0.0,<5.0a0 - - libdeflate >=1.23,<1.24.0a0 - - libgcc >=13 - - libjpeg-turbo >=3.0.0,<4.0a0 - - liblzma >=5.6.3,<6.0a0 - - libstdcxx >=13 - - libwebp-base >=1.4.0,<2.0a0 - - libzlib >=1.3.1,<2.0a0 - - zstd >=1.5.6,<1.6.0a0 - license: HPND - purls: [] - size: 428173 - timestamp: 1734398813264 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libtiff-4.7.0-h88f7998_3.conda - sha256: 5888bd66ba7606ae8596856c7dac800940ecad0aed77d6aa37db69d434c81cf0 - md5: 36a0ea4a173338c8725dc0807e99cf22 - depends: - - lerc >=4.0.0,<5.0a0 - - libdeflate >=1.23,<1.24.0a0 - - libgcc >=13 - - libjpeg-turbo >=3.0.0,<4.0a0 - - liblzma >=5.6.3,<6.0a0 - - libstdcxx >=13 - - libwebp-base >=1.4.0,<2.0a0 - - libzlib >=1.3.1,<2.0a0 - - zstd >=1.5.6,<1.6.0a0 - license: HPND - purls: [] - size: 464699 - timestamp: 1734398752249 - conda: https://conda.anaconda.org/conda-forge/linux-64/libunistring-0.9.10-h7f98852_0.tar.bz2 sha256: e88c45505921db29c08df3439ddb7f771bbff35f95e7d3103bf365d5d6ce2a6d md5: 7245a044b4a1980ed83196176b78b73a @@ -3896,58 +3269,6 @@ packages: purls: [] size: 621056 timestamp: 1737016626950 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.5.0-h851e524_0.conda - sha256: c45283fd3e90df5f0bd3dbcd31f59cdd2b001d424cf30a07223655413b158eaf - md5: 63f790534398730f59e1b899c3644d4a - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - constrains: - - libwebp 1.5.0 - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 429973 - timestamp: 1734777489810 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libwebp-base-1.5.0-h0886dbf_0.conda - sha256: b3d881a0ae08bb07fff7fa8ead506c8d2e0388733182fe4f216f3ec5d61ffcf0 - md5: 95ef4a689b8cc1b7e18b53784d88f96b - depends: - - libgcc >=13 - constrains: - - libwebp 1.5.0 - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 362623 - timestamp: 1734779054659 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.17.0-h8a09558_0.conda - sha256: 666c0c431b23c6cec6e492840b176dde533d48b7e6fb8883f5071223433776aa - md5: 92ed62436b625154323d40d5f2f11dd7 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - pthread-stubs - - xorg-libxau >=1.0.11,<2.0a0 - - xorg-libxdmcp - license: MIT - license_family: MIT - purls: [] - size: 395888 - timestamp: 1727278577118 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libxcb-1.17.0-h262b8f6_0.conda - sha256: 461cab3d5650ac6db73a367de5c8eca50363966e862dcf60181d693236b1ae7b - md5: cd14ee5cca2464a425b1dbfc24d90db2 - depends: - - libgcc >=13 - - pthread-stubs - - xorg-libxau >=1.0.11,<2.0a0 - - xorg-libxdmcp - license: MIT - license_family: MIT - purls: [] - size: 397493 - timestamp: 1727280745441 - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda sha256: 6ae68e0b86423ef188196fff6207ed0c8195dd84273cb5623b85aa08033a410c md5: 5aa797f8787fe7a17d1b0821485b5adc @@ -3966,35 +3287,6 @@ packages: purls: [] size: 114269 timestamp: 1702724369203 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.13.6-h8d12d68_0.conda - sha256: db8af71ea9c0ae95b7cb4a0f59319522ed2243942437a1200ceb391493018d85 - md5: 328382c0e0ca648e5c189d5ec336c604 - depends: - - __glibc >=2.17,<3.0.a0 - - icu >=75.1,<76.0a0 - - libgcc >=13 - - libiconv >=1.18,<2.0a0 - - liblzma >=5.6.4,<6.0a0 - - libzlib >=1.3.1,<2.0a0 - license: MIT - license_family: MIT - purls: [] - size: 690296 - timestamp: 1739952967309 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libxml2-2.13.6-h2e0c361_0.conda - sha256: bb075df08b04b1b47e75a250f2ebbb2401c3367057d64b8d44ef1ef3f44480e1 - md5: a159a92f890f862408c951c08f13415f - depends: - - icu >=75.1,<76.0a0 - - libgcc >=13 - - libiconv >=1.18,<2.0a0 - - liblzma >=5.6.4,<6.0a0 - - libzlib >=1.3.1,<2.0a0 - license: MIT - license_family: MIT - purls: [] - size: 733707 - timestamp: 1739953178456 - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda sha256: d4bfe88d7cb447768e31650f06257995601f89076080e76df55e3112d4e47dc4 md5: edb0dca6bc32e4f4789199455a1dbeb8 @@ -4020,72 +3312,30 @@ packages: purls: [] size: 66657 timestamp: 1727963199518 -- conda: https://conda.anaconda.org/conda-forge/noarch/linkify-it-py-2.0.3-pyhd8ed1ab_1.conda - sha256: d975a2015803d4fdaaae3f53e21f64996577d7a069eb61c6d2792504f16eb57b - md5: b02fe519b5dc0dc55e7299810fcdfb8e - depends: - - python >=3.9 +- pypi: https://files.pythonhosted.org/packages/b4/de/88b3be5c31b22333b3ca2f6ff1de4e863d8fe45aaea7485f591970ec1d3e/linkify_it_py-2.1.0-py3-none-any.whl + name: linkify-it-py + version: 2.1.0 + sha256: 0d252c1594ecba2ecedc444053db5d3a9b7ec1b0dd929c8f1d74dce89f86c05e + requires_dist: - uc-micro-py - license: MIT - license_family: MIT - purls: - - pkg:pypi/linkify-it-py?source=hash-mapping - size: 24154 - timestamp: 1733781296133 -- conda: https://conda.anaconda.org/conda-forge/noarch/lockfile-0.12.2-py_1.tar.bz2 - sha256: d3a68045ef74a2a7b8c8a55b242fdbc875d362e37adcf793613cf0d8c8e4fbf7 - md5: c104d98e09c47519950cffb8dd5b4f10 - depends: - - python - license: MIT - license_family: MIT - purls: - - pkg:pypi/lockfile?source=hash-mapping - size: 10856 - timestamp: 1531372274693 -- conda: https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.10.0-h5888daf_1.conda - sha256: 47326f811392a5fd3055f0f773036c392d26fdb32e4d8e7a8197eed951489346 - md5: 9de5350a85c4a20c685259b889aa6393 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - libstdcxx >=13 - license: BSD-2-Clause - license_family: BSD - purls: [] - size: 167055 - timestamp: 1733741040117 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/lz4-c-1.10.0-h5ad3122_1.conda - sha256: 67e55058d275beea76c1882399640c37b5be8be4eb39354c94b610928e9a0573 - md5: 6654e411da94011e8fbe004eacb8fe11 - depends: - - libgcc >=13 - - libstdcxx >=13 - license: BSD-2-Clause - license_family: BSD - purls: [] - size: 184953 - timestamp: 1733740984533 -- conda: https://conda.anaconda.org/conda-forge/linux-64/lzo-2.10-hd590300_1001.conda - sha256: 88433b98a9dd9da315400e7fb9cd5f70804cb17dca8b1c85163a64f90f584126 - md5: ec7398d21e2651e0dcb0044d03b9a339 - depends: - - libgcc-ng >=12 - license: GPL-2.0-or-later - license_family: GPL2 - purls: [] - size: 171416 - timestamp: 1713515738503 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/lzo-2.10-h31becfc_1001.conda - sha256: d8626d739ac4268e63ca4ba71329cfc4da78b59b377b8cb45a81840138e0e3c9 - md5: 004025fe20a11090e0b02154f413a758 - depends: - - libgcc-ng >=12 - license: GPL-2.0-or-later - license_family: GPL2 - purls: [] - size: 164049 - timestamp: 1713517023523 + - pytest ; extra == 'test' + - coverage ; extra == 'test' + - pytest-cov ; extra == 'test' + - pre-commit ; extra == 'dev' + - isort ; extra == 'dev' + - flake8 ; extra == 'dev' + - black ; extra == 'dev' + - pyproject-flake8 ; extra == 'dev' + - pytest ; extra == 'benchmark' + - pytest-benchmark ; extra == 'benchmark' + - sphinx ; extra == 'doc' + - sphinx-book-theme ; extra == 'doc' + - myst-parser ; extra == 'doc' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/c8/22/9460e311f340cb62d26a38c419b1381b8593b0bb6b5d1f056938b086d362/lockfile-0.12.2-py2.py3-none-any.whl + name: lockfile + version: 0.12.2 + sha256: 6c3cb24f344923d30b2785d5ad75182c8ea7ac1b6171b08657258ec7429d50fa - conda: https://conda.anaconda.org/conda-forge/noarch/mako-1.3.9-pyhd8ed1ab_0.conda sha256: 56ac22e0800b44600662de49f8bc241b2d785820e44d96eebb6eae7e072c8a99 md5: 422113c902cc5181ccaafbb4b827e492 @@ -4099,30 +3349,55 @@ packages: - pkg:pypi/mako?source=hash-mapping size: 67008 timestamp: 1738719687521 -- conda: https://conda.anaconda.org/conda-forge/noarch/markdown-3.6-pyhd8ed1ab_0.conda - sha256: fce1fde00359696983989699c00f9891194c4ebafea647a8d21b7e2e3329b56e - md5: 06e9bebf748a0dea03ecbe1f0e27e909 - depends: - - importlib-metadata >=4.4 - - python >=3.6 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/markdown?source=hash-mapping - size: 78331 - timestamp: 1710435316163 -- conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-3.0.0-pyhd8ed1ab_1.conda - sha256: 0fbacdfb31e55964152b24d5567e9a9996e1e7902fb08eb7d91b5fd6ce60803a - md5: fee3164ac23dfca50cfcc8b85ddefb81 - depends: - - mdurl >=0.1,<1 - - python >=3.9 - license: MIT - license_family: MIT - purls: - - pkg:pypi/markdown-it-py?source=hash-mapping - size: 64430 - timestamp: 1733250550053 +- pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl + name: markdown + version: 3.10.2 + sha256: e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36 + requires_dist: + - coverage ; extra == 'testing' + - pyyaml ; extra == 'testing' + - mkdocs>=1.6 ; extra == 'docs' + - mkdocs-nature>=0.6 ; extra == 'docs' + - mdx-gh-links>=0.2 ; extra == 'docs' + - mkdocstrings[python]>=0.28.3 ; extra == 'docs' + - mkdocs-gen-files ; extra == 'docs' + - mkdocs-section-index ; extra == 'docs' + - mkdocs-literate-nav ; extra == 'docs' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl + name: markdown-it-py + version: 4.2.0 + sha256: 9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a + requires_dist: + - mdurl~=0.1 + - psutil ; extra == 'benchmarking' + - pytest ; extra == 'benchmarking' + - pytest-benchmark ; extra == 'benchmarking' + - commonmark~=0.9 ; extra == 'compare' + - markdown~=3.4 ; extra == 'compare' + - mistletoe~=1.0 ; extra == 'compare' + - mistune~=3.0 ; extra == 'compare' + - panflute~=2.3 ; extra == 'compare' + - markdown-it-pyrs ; extra == 'compare' + - linkify-it-py>=1,<3 ; extra == 'linkify' + - mdit-py-plugins>=0.5.0 ; extra == 'plugins' + - gprof2dot ; extra == 'profiling' + - mdit-py-plugins>=0.5.0 ; extra == 'rtd' + - myst-parser ; extra == 'rtd' + - pyyaml ; extra == 'rtd' + - sphinx ; extra == 'rtd' + - sphinx-copybutton ; extra == 'rtd' + - sphinx-design ; extra == 'rtd' + - sphinx-book-theme~=1.0 ; extra == 'rtd' + - jupyter-sphinx ; extra == 'rtd' + - ipykernel ; extra == 'rtd' + - coverage ; extra == 'testing' + - pytest ; extra == 'testing' + - pytest-cov ; extra == 'testing' + - pytest-regressions ; extra == 'testing' + - pytest-timeout ; extra == 'testing' + - requests ; extra == 'testing' + requires_python: '>=3.10' - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.2-py310h89163eb_1.conda sha256: 0bed20ec27dcbcaf04f02b2345358e1161fb338f8423a4ada1cf0f4d46918741 md5: 8ce3f0332fd6de0d737e2911d329523f @@ -4154,94 +3429,60 @@ packages: - pkg:pypi/markupsafe?source=hash-mapping size: 23294 timestamp: 1733220959789 -- conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.1.7-pyhd8ed1ab_1.conda - sha256: 69b7dc7131703d3d60da9b0faa6dd8acbf6f6c396224cf6aef3e855b8c0c41c6 - md5: af6ab708897df59bd6e7283ceab1b56b - depends: - - python >=3.9 +- pypi: https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl + name: matplotlib-inline + version: 0.2.1 + sha256: d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76 + requires_dist: - traitlets - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/matplotlib-inline?source=hash-mapping - size: 14467 - timestamp: 1733417051523 -- conda: https://conda.anaconda.org/conda-forge/noarch/mdit-py-plugins-0.4.2-pyhd8ed1ab_1.conda - sha256: c63ed79d9745109c0a70397713b0c07f06e7d3561abcb122cfc80a141ab3b449 - md5: af2060041d4f3250a7eb6ab3ec0e549b - depends: - - markdown-it-py >=1.0.0,<4.0.0 - - python >=3.9 - license: MIT - license_family: MIT - purls: - - pkg:pypi/mdit-py-plugins?source=hash-mapping - size: 42180 - timestamp: 1733854816517 -- conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda - sha256: 78c1bbe1723449c52b7a9df1af2ee5f005209f67e40b6e1d3c7619127c43b1c7 - md5: 592132998493b3ff25fd7479396e8351 - depends: - - python >=3.9 - license: MIT - license_family: MIT - purls: - - pkg:pypi/mdurl?source=hash-mapping - size: 14465 - timestamp: 1733255681319 -- conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.1.2-pyhd8ed1ab_0.conda - sha256: 63d5308ac732b2f8130702c83ee40ce31c5451ebcb6e70075b771cc8f7df0156 - md5: 0982b0f06168fe3421d09f70596ca1f0 - depends: - - python >=3.9 - - typing_extensions - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/mistune?source=compressed-mapping - size: 68903 - timestamp: 1739952304731 -- conda: https://conda.anaconda.org/conda-forge/noarch/more-itertools-10.6.0-pyhd8ed1ab_0.conda - sha256: e017ede184823b12a194d058924ca26e1129975cee1cae47f69d6115c0478b55 - md5: 9b1225d67235df5411dbd2c94a5876b7 - depends: - - python >=3.9 - license: MIT - license_family: MIT - purls: - - pkg:pypi/more-itertools?source=hash-mapping - size: 58739 - timestamp: 1736883940984 -- conda: https://conda.anaconda.org/conda-forge/linux-64/msgpack-python-1.1.0-py310h3788b33_0.conda - sha256: 73ca5f0c7d0727a57dcc3c402823ce3aa159ca075210be83078fcc485971e259 - md5: 6b586fb03d84e5bfbb1a8a3d9e2c9b60 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - libstdcxx >=13 - - python >=3.10,<3.11.0a0 - - python_abi 3.10.* *_cp310 - license: Apache-2.0 - license_family: Apache - purls: - - pkg:pypi/msgpack?source=hash-mapping - size: 98083 - timestamp: 1725975111763 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/msgpack-python-1.1.0-py310hf54e67a_0.conda - sha256: c35492fe56b3dc503e7177d9fd9cc9492ac25015ad0e64e1c769ad6b6e80a05b - md5: e946a50c0d24b6afb61a101b76d800bc - depends: - - libgcc >=13 - - libstdcxx >=13 - - python >=3.10,<3.11.0a0 - - python >=3.10,<3.11.0a0 *_cpython - - python_abi 3.10.* *_cp310 - license: Apache-2.0 - license_family: Apache - purls: - - pkg:pypi/msgpack?source=hash-mapping - size: 95776 - timestamp: 1725975295832 + - flake8 ; extra == 'test' + - nbdime ; extra == 'test' + - nbval ; extra == 'test' + - notebook ; extra == 'test' + - pytest ; extra == 'test' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/71/d6/48f5b9e44e2e760855d7b489b1317cd7620e82dcb73197961e5cc1391348/mdit_py_plugins-0.6.0-py3-none-any.whl + name: mdit-py-plugins + version: 0.6.0 + sha256: f7e7a25d8b616fee99cb1e330da73451d11a8061baf39bb9663ab9ce0e005b90 + requires_dist: + - markdown-it-py>=2.0.0,<5.0.0 + - pre-commit ; extra == 'code-style' + - myst-parser ; extra == 'rtd' + - sphinx-book-theme ; extra == 'rtd' + - coverage ; extra == 'testing' + - pytest ; extra == 'testing' + - pytest-cov ; extra == 'testing' + - pytest-regressions ; extra == 'testing' + - pytest-timeout ; extra == 'testing' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl + name: mdurl + version: 0.1.2 + sha256: 84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/2a/7f/a946aa4f8752b37102b41e64dca18a1976ac705c3a0d1dfe74d820a02552/mistune-3.2.1-py3-none-any.whl + name: mistune + version: 3.2.1 + sha256: 78cdb0ba5e938053ccf63651b352508d2efa9411dc8810bfb05f2dc5140c0048 + requires_dist: + - typing-extensions ; python_full_version < '3.11' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/cb/98/6af411189d9413534c3eb691182bff1f5c6d44ed2f93f2edfe52a1bbceb8/more_itertools-11.0.2-py3-none-any.whl + name: more-itertools + version: 11.0.2 + sha256: 6e35b35f818b01f691643c6c611bc0902f2e92b46c18fffa77ae1e7c46e912e4 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/71/e5/c2241de64bfceac456b140737812a2ab310b10538a7b34a1d393b748e095/msgpack-1.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + name: msgpack + version: 1.1.2 + sha256: 8b696e83c9f1532b4af884045ba7f3aa741a63b2bc22617293a2c6a7c645f251 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/b7/09/2a06956383c0fdebaef5aa9246e2356776f12ea6f2a44bd1368abf0e46c4/msgpack-1.1.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: msgpack + version: 1.1.2 + sha256: 365c0bbe981a27d8932da71af63ef86acc59ed5c01ad929e09a0b88c6294e28a + requires_python: '>=3.9' - conda: https://conda.anaconda.org/conda-forge/linux-64/multidict-6.2.0-py310h89163eb_0.conda sha256: dc678195b6d5e3beae5a0df107bcc310ecc7e93e0ac9d67b57ee9eb729090760 md5: b58e297cc037aba6f32edef76fd0e49a @@ -4270,66 +3511,136 @@ packages: - pkg:pypi/multidict?source=hash-mapping size: 62769 timestamp: 1742308314182 -- conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.2-pyhd8ed1ab_0.conda - sha256: a20cff739d66c2f89f413e4ba4c6f6b59c50d5c30b5f0d840c13e8c9c2df9135 - md5: 6bb0d77277061742744176ab555b723c - depends: - - jupyter_client >=6.1.12 - - jupyter_core >=4.12,!=5.0.* - - nbformat >=5.1 - - python >=3.8 - - traitlets >=5.4 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/nbclient?source=hash-mapping - size: 28045 - timestamp: 1734628936013 -- conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.16.6-pyh29332c3_0.conda - sha256: dcccb07c5a1acb7dc8be94330e62d54754c0e9c9cb2bb6865c8e3cfe44cf5a58 - md5: d24beda1d30748afcc87c429454ece1b - depends: +- pypi: https://files.pythonhosted.org/packages/c7/e1/68c2256b69a314eba133673377ba9118c356f6342a0c02b61de449cf2bf2/narwhals-2.21.0-py3-none-any.whl + name: narwhals + version: 2.21.0 + sha256: 1e6617d0fca68ae1fda29e5397c4eaacd3ffc9fffe6bcd6ded0c690475e853be + requires_dist: + - cudf-cu12>=24.10.0 ; extra == 'cudf' + - dask[dataframe]>=2024.8 ; extra == 'dask' + - duckdb>=1.1 ; extra == 'duckdb' + - ibis-framework>=6.0.0 ; extra == 'ibis' + - packaging ; extra == 'ibis' + - pyarrow-hotfix ; extra == 'ibis' + - rich ; extra == 'ibis' + - modin ; extra == 'modin' + - pandas>=1.1.3 ; extra == 'pandas' + - polars>=0.20.4 ; extra == 'polars' + - pyarrow>=13.0.0 ; extra == 'pyarrow' + - pyspark>=3.5.0 ; extra == 'pyspark' + - pyspark[connect]>=3.5.0 ; extra == 'pyspark-connect' + - duckdb>=1.1 ; extra == 'sql' + - sqlparse ; extra == 'sql' + - sqlframe>=3.22.0,!=3.39.3 ; extra == 'sqlframe' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/83/a0/5b0c2f11142ed1dddec842457d3f65eaf71a0080894eb6f018755b319c3a/nbclient-0.10.4-py3-none-any.whl + name: nbclient + version: 0.10.4 + sha256: 9162df5a7373d70d606527300a95a975a47c137776cd942e52d9c7e29ff83440 + requires_dist: + - jupyter-client>=6.1.12 + - jupyter-core>=4.12,!=5.0.* + - nbformat>=5.1.3 + - traitlets>=5.4 + - pre-commit ; extra == 'dev' + - autodoc-traits ; extra == 'docs' + - flaky ; extra == 'docs' + - ipykernel>=6.19.3 ; extra == 'docs' + - ipython ; extra == 'docs' + - ipywidgets ; extra == 'docs' + - mock ; extra == 'docs' + - moto ; extra == 'docs' + - myst-parser ; extra == 'docs' + - nbconvert>=7.1.0 ; extra == 'docs' + - pytest-asyncio>=1.3.0 ; extra == 'docs' + - pytest-cov>=4.0 ; extra == 'docs' + - pytest>=9.0.1,<10 ; extra == 'docs' + - sphinx-book-theme ; extra == 'docs' + - sphinx>=1.7 ; extra == 'docs' + - sphinxcontrib-spelling ; extra == 'docs' + - testpath ; extra == 'docs' + - xmltodict ; extra == 'docs' + - flaky ; extra == 'test' + - ipykernel>=6.19.3 ; extra == 'test' + - ipython ; extra == 'test' + - ipywidgets ; extra == 'test' + - nbconvert>=7.1.0 ; extra == 'test' + - pytest-asyncio>=1.3.0 ; extra == 'test' + - pytest-cov>=4.0 ; extra == 'test' + - pytest>=9.0.1,<10 ; extra == 'test' + - testpath ; extra == 'test' + - xmltodict ; extra == 'test' + requires_python: '>=3.10.0' +- pypi: https://files.pythonhosted.org/packages/67/f8/bb0a9d5f46819c821dc1f004aa2cc29b1d91453297dbf5ff20470f00f193/nbconvert-7.17.1-py3-none-any.whl + name: nbconvert + version: 7.17.1 + sha256: aa85c087b435e7bf1ffd03319f658e285f2b89eccab33bc1ba7025495ab3e7c8 + requires_dist: - beautifulsoup4 - - bleach-with-css !=5.0.0 + - bleach[css]!=5.0.0 - defusedxml - - importlib-metadata >=3.6 - - jinja2 >=3.0 - - jupyter_core >=4.7 - - jupyterlab_pygments - - markupsafe >=2.0 - - mistune >=2.0.3,<4 - - nbclient >=0.5.0 - - nbformat >=5.7 + - importlib-metadata>=3.6 ; python_full_version < '3.10' + - jinja2>=3.0 + - jupyter-core>=4.7 + - jupyterlab-pygments + - markupsafe>=2.0 + - mistune>=2.0.3,<4 + - nbclient>=0.5.0 + - nbformat>=5.7 - packaging - - pandocfilters >=1.4.1 - - pygments >=2.4.1 - - python >=3.9 - - traitlets >=5.1 - - python - constrains: - - pandoc >=2.9.2,<4.0.0 - - nbconvert ==7.16.6 *_0 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/nbconvert?source=hash-mapping - size: 200601 - timestamp: 1738067871724 -- conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda - sha256: 7a5bd30a2e7ddd7b85031a5e2e14f290898098dc85bea5b3a5bf147c25122838 - md5: bbe1963f1e47f594070ffe87cdf612ea - depends: - - jsonschema >=2.6 - - jupyter_core >=4.12,!=5.0.* - - python >=3.9 - - python-fastjsonschema >=2.15 - - traitlets >=5.1 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/nbformat?source=hash-mapping - size: 100945 - timestamp: 1733402844974 + - pandocfilters>=1.4.1 + - pygments>=2.4.1 + - traitlets>=5.1 + - flaky ; extra == 'all' + - intersphinx-registry ; extra == 'all' + - ipykernel ; extra == 'all' + - ipython ; extra == 'all' + - ipywidgets>=7.5 ; extra == 'all' + - myst-parser ; extra == 'all' + - nbsphinx>=0.2.12 ; extra == 'all' + - playwright ; extra == 'all' + - pydata-sphinx-theme ; extra == 'all' + - pyqtwebengine>=5.15 ; extra == 'all' + - pytest>=7 ; extra == 'all' + - sphinx>=5.0.2 ; extra == 'all' + - sphinxcontrib-spelling ; extra == 'all' + - tornado>=6.1 ; extra == 'all' + - intersphinx-registry ; extra == 'docs' + - ipykernel ; extra == 'docs' + - ipython ; extra == 'docs' + - myst-parser ; extra == 'docs' + - nbsphinx>=0.2.12 ; extra == 'docs' + - pydata-sphinx-theme ; extra == 'docs' + - sphinx>=5.0.2 ; extra == 'docs' + - sphinxcontrib-spelling ; extra == 'docs' + - pyqtwebengine>=5.15 ; extra == 'qtpdf' + - pyqtwebengine>=5.15 ; extra == 'qtpng' + - tornado>=6.1 ; extra == 'serve' + - flaky ; extra == 'test' + - ipykernel ; extra == 'test' + - ipywidgets>=7.5 ; extra == 'test' + - pytest>=7 ; extra == 'test' + - playwright ; extra == 'webpdf' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl + name: nbformat + version: 5.10.4 + sha256: 3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b + requires_dist: + - fastjsonschema>=2.15 + - jsonschema>=2.6 + - jupyter-core>=4.12,!=5.0.* + - traitlets>=5.1 + - myst-parser ; extra == 'docs' + - pydata-sphinx-theme ; extra == 'docs' + - sphinx ; extra == 'docs' + - sphinxcontrib-github-alt ; extra == 'docs' + - sphinxcontrib-spelling ; extra == 'docs' + - pep440 ; extra == 'test' + - pre-commit ; extra == 'test' + - pytest ; extra == 'test' + - testpath ; extra == 'test' + requires_python: '>=3.8' - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda sha256: 3fde293232fa3fca98635e1167de6b7c7fda83caf24b9d6c91ec9eefb4f4d586 md5: 47e340acb35de30501a76c7c799c41d7 @@ -4356,17 +3667,11 @@ packages: requires_dist: - jupyterhub>=5.0.0 requires_python: '>=3.8' -- conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda - sha256: bb7b21d7fd0445ddc0631f64e66d91a179de4ba920b8381f29b9d006a42788c0 - md5: 598fd7d4d0de2455fb74f56063969a97 - depends: - - python >=3.9 - license: BSD-2-Clause - license_family: BSD - purls: - - pkg:pypi/nest-asyncio?source=hash-mapping - size: 11543 - timestamp: 1733325673691 +- pypi: https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl + name: nest-asyncio + version: 1.6.0 + sha256: 87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c + requires_python: '>=3.5' - conda: https://conda.anaconda.org/conda-forge/linux-64/nodejs-22.13.0-hf235a45_0.conda sha256: 925ea8839d6f26d0eb4204675b98a862803a9a9657fd36a4a22c4c29a479a911 md5: 1f9efd96347aa008bd2c735d7d88fc75 @@ -4401,75 +3706,56 @@ packages: purls: [] size: 22156450 timestamp: 1737394666729 -- conda: https://conda.anaconda.org/conda-forge/noarch/notebook-7.3.3-pyhd8ed1ab_0.conda - sha256: 5086c70ff352a72b9d47fcf73d37a1be583cf5b416c9729295a9b3710330d781 - md5: 3b04a08fc654590f45e0a713982f898b - depends: - - importlib_resources >=5.0 - - jupyter_server >=2.4.0,<3 - - jupyterlab >=4.3.6,<4.4 - - jupyterlab_server >=2.27.1,<3 - - notebook-shim >=0.2,<0.3 - - python >=3.9 - - tornado >=6.2.0 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/notebook?source=hash-mapping - size: 9705127 - timestamp: 1741968301453 -- conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_1.conda - sha256: 7b920e46b9f7a2d2aa6434222e5c8d739021dbc5cc75f32d124a8191d86f9056 - md5: e7f89ea5f7ea9401642758ff50a2d9c1 - depends: - - jupyter_server >=1.8,<3 - - python >=3.9 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/notebook-shim?source=hash-mapping - size: 16817 - timestamp: 1733408419340 -- conda: https://conda.anaconda.org/conda-forge/linux-64/numpy-2.2.4-py310hefbff90_0.conda - sha256: 98d7fc28869de4a43909e36317f42a1c8b2c131315b43b0d74077422b70682c3 - md5: b3a99849aa14b78d32250c0709e8792a - depends: - - __glibc >=2.17,<3.0.a0 - - libblas >=3.9.0,<4.0a0 - - libcblas >=3.9.0,<4.0a0 - - libgcc >=13 - - liblapack >=3.9.0,<4.0a0 - - libstdcxx >=13 - - python >=3.10,<3.11.0a0 - - python_abi 3.10.* *_cp310 - constrains: - - numpy-base <0a0 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/numpy?source=hash-mapping - size: 7981846 - timestamp: 1742255356889 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/numpy-2.2.4-py310h6e5608f_0.conda - sha256: 01013f2dcd24cd9bea66b079cbf98deefddec4d1a176c33744494ebe6991c070 - md5: 3a7b45aaa7704194b823d2d34b75aad1 - depends: - - libblas >=3.9.0,<4.0a0 - - libcblas >=3.9.0,<4.0a0 - - libgcc >=13 - - liblapack >=3.9.0,<4.0a0 - - libstdcxx >=13 - - python >=3.10,<3.11.0a0 - - python >=3.10,<3.11.0a0 *_cpython - - python_abi 3.10.* *_cp310 - constrains: - - numpy-base <0a0 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/numpy?source=hash-mapping - size: 6679126 - timestamp: 1742255398605 +- pypi: https://files.pythonhosted.org/packages/e9/d6/1fd0646b9bbd9efbb0b8ae21b2325fbef515769a5621c03e31d8eb8da587/notebook-7.5.6-py3-none-any.whl + name: notebook + version: 7.5.6 + sha256: 4dde3f8fb55fa8fb7946d58c6e869ce9baf46d00fc070664f62604569d0faca0 + requires_dist: + - jupyter-server>=2.4.0,<3 + - jupyterlab-server>=2.28.0,<3 + - jupyterlab>=4.5.7,<4.6 + - notebook-shim>=0.2,<0.3 + - tornado>=6.2.0 + - hatch ; extra == 'dev' + - pre-commit ; extra == 'dev' + - myst-parser ; extra == 'docs' + - nbsphinx ; extra == 'docs' + - pydata-sphinx-theme ; extra == 'docs' + - sphinx>=1.3.6 ; extra == 'docs' + - sphinxcontrib-github-alt ; extra == 'docs' + - sphinxcontrib-spelling ; extra == 'docs' + - importlib-resources>=5.0 ; python_full_version < '3.10' and extra == 'test' + - ipykernel ; extra == 'test' + - jupyter-server[test]>=2.4.0,<3 ; extra == 'test' + - jupyterlab-server[test]>=2.28.0,<3 ; extra == 'test' + - nbval ; extra == 'test' + - pytest-console-scripts ; extra == 'test' + - pytest-timeout ; extra == 'test' + - pytest-tornasync ; extra == 'test' + - pytest>=7.0 ; extra == 'test' + - requests ; extra == 'test' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl + name: notebook-shim + version: 0.2.4 + sha256: 411a5be4e9dc882a074ccbcae671eda64cceb068767e9a3419096986560e1cef + requires_dist: + - jupyter-server>=1.8,<3 + - pytest ; extra == 'test' + - pytest-console-scripts ; extra == 'test' + - pytest-jupyter ; extra == 'test' + - pytest-tornasync ; extra == 'test' + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: numpy + version: 2.2.6 + sha256: fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl + name: numpy + version: 2.2.6 + sha256: efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83 + requires_python: '>=3.10' - conda: https://conda.anaconda.org/conda-forge/noarch/oauthenticator-16.3.0-pyhd8ed1ab_0.conda sha256: 9c72a8a13c2cfc65c08d27091cfb052f79d9ac03cf6d6862677dc0dd4ee85d6d md5: ae7d293fcd41a5ed78b12c0b5c0b03a3 @@ -4502,35 +3788,6 @@ packages: - pkg:pypi/oauthlib?source=hash-mapping size: 97604 timestamp: 1733752957557 -- conda: https://conda.anaconda.org/conda-forge/linux-64/openjpeg-2.5.3-h5fbd93e_0.conda - sha256: 5bee706ea5ba453ed7fd9da7da8380dd88b865c8d30b5aaec14d2b6dd32dbc39 - md5: 9e5816bc95d285c115a3ebc2f8563564 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - libpng >=1.6.44,<1.7.0a0 - - libstdcxx >=13 - - libtiff >=4.7.0,<4.8.0a0 - - libzlib >=1.3.1,<2.0a0 - license: BSD-2-Clause - license_family: BSD - purls: [] - size: 342988 - timestamp: 1733816638720 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/openjpeg-2.5.3-h3f56577_0.conda - sha256: 92d310033e20538e896f4e4b1ea4205eb6604eee7c5c651c4965a0d8d3ca0f1d - md5: 04231368e4af50d11184b50e14250993 - depends: - - libgcc >=13 - - libpng >=1.6.44,<1.7.0a0 - - libstdcxx >=13 - - libtiff >=4.7.0,<4.8.0a0 - - libzlib >=1.3.1,<2.0a0 - license: BSD-2-Clause - license_family: BSD - purls: [] - size: 377796 - timestamp: 1733816683252 - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.1-h35e630c_1.conda sha256: 44c877f8af015332a5d12f5ff0fb20ca32f896526a7d0cdb30c769df1144fb5c md5: f61eb8cd60ff9057122a3d338b99c00f @@ -4554,18 +3811,13 @@ packages: purls: [] size: 3692030 timestamp: 1769557678657 -- conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda - sha256: 1840bd90d25d4930d60f57b4f38d4e0ae3f5b8db2819638709c36098c6ba770c - md5: e51f1e4089cad105b6cac64bd8166587 - depends: - - python >=3.9 - - typing_utils - license: Apache-2.0 - license_family: APACHE - purls: - - pkg:pypi/overrides?source=hash-mapping - size: 30139 - timestamp: 1734587755455 +- pypi: https://files.pythonhosted.org/packages/2c/ab/fc8290c6a4c722e5514d80f62b2dc4c4df1a68a41d1364e625c35990fcf3/overrides-7.7.0-py3-none-any.whl + name: overrides + version: 7.7.0 + sha256: c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49 + requires_dist: + - typing ; python_full_version < '3.5' + requires_python: '>=3.6' - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda sha256: da157b19bcd398b9804c5c52fc000fcb8ab0525bdb9c70f95beaa0bb42f85af1 md5: 3bfed7e6228ebf2f7b9eaa47f1b4e2aa @@ -4585,152 +3837,325 @@ packages: license: MIT license_family: MIT purls: - - pkg:pypi/pamela?source=hash-mapping - size: 12522 - timestamp: 1734511312340 -- conda: https://conda.anaconda.org/conda-forge/linux-64/pandas-2.2.3-py310h5eaa309_1.conda - sha256: d772223fd1ca882717ec6db55a13a6be9439c64ca3532231855ce7834599b8a5 - md5: e67778e1cac3bca3b3300f6164f7ffb9 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - libstdcxx >=13 - - numpy >=1.19,<3 - - numpy >=1.22.4 - - python >=3.10,<3.11.0a0 - - python-dateutil >=2.8.1 - - python-tzdata >=2022a - - python_abi 3.10.* *_cp310 - - pytz >=2020.1,<2024.2 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/pandas?source=hash-mapping - size: 13014228 - timestamp: 1726878893275 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pandas-2.2.3-py310hce852f7_1.conda - sha256: 4dfc71ad8b54c250439e08ff66bc39c1e40124cdb53a6d06a4e7ef029befc44f - md5: 28fd41b43810af3f3efbe52f11428b03 - depends: - - libgcc >=13 - - libstdcxx >=13 - - numpy >=1.19,<3 - - numpy >=1.22.4 - - python >=3.10,<3.11.0a0 - - python >=3.10,<3.11.0a0 *_cpython - - python-dateutil >=2.8.1 - - python-tzdata >=2022a - - python_abi 3.10.* *_cp310 - - pytz >=2020.1,<2024.2 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/pandas?source=hash-mapping - size: 12617107 - timestamp: 1726879128981 -- conda: https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 - sha256: 2bb9ba9857f4774b85900c2562f7e711d08dd48e2add9bee4e1612fbee27e16f - md5: 457c2c8c08e54905d6954e79cb5b5db9 - depends: - - python !=3.0,!=3.1,!=3.2,!=3.3 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/pandocfilters?source=hash-mapping - size: 11627 - timestamp: 1631603397334 -- conda: https://conda.anaconda.org/conda-forge/noarch/panel-1.6.1-pyhd8ed1ab_0.conda - sha256: 2decd60c2276d818ca23b134491644fe067746e11dc1501dfdafc3e523564a3c - md5: e52807f99bbd86f6aef02c1c954fc1c1 - depends: + - pkg:pypi/pamela?source=hash-mapping + size: 12522 + timestamp: 1734511312340 +- pypi: https://files.pythonhosted.org/packages/1d/03/3fc4a529a7710f890a239cc496fc6d50ad4a0995657dccc1d64695adb9f4/pandas-2.3.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl + name: pandas + version: 2.3.3 + sha256: 5caf26f64126b6c7aec964f74266f435afef1c1b13da3b0636c7518a1fa3e2b1 + requires_dist: + - numpy>=1.22.4 ; python_full_version < '3.11' + - numpy>=1.23.2 ; python_full_version == '3.11.*' + - numpy>=1.26.0 ; python_full_version >= '3.12' + - python-dateutil>=2.8.2 + - pytz>=2020.1 + - tzdata>=2022.7 + - hypothesis>=6.46.1 ; extra == 'test' + - pytest>=7.3.2 ; extra == 'test' + - pytest-xdist>=2.2.0 ; extra == 'test' + - pyarrow>=10.0.1 ; extra == 'pyarrow' + - bottleneck>=1.3.6 ; extra == 'performance' + - numba>=0.56.4 ; extra == 'performance' + - numexpr>=2.8.4 ; extra == 'performance' + - scipy>=1.10.0 ; extra == 'computation' + - xarray>=2022.12.0 ; extra == 'computation' + - fsspec>=2022.11.0 ; extra == 'fss' + - s3fs>=2022.11.0 ; extra == 'aws' + - gcsfs>=2022.11.0 ; extra == 'gcp' + - pandas-gbq>=0.19.0 ; extra == 'gcp' + - odfpy>=1.4.1 ; extra == 'excel' + - openpyxl>=3.1.0 ; extra == 'excel' + - python-calamine>=0.1.7 ; extra == 'excel' + - pyxlsb>=1.0.10 ; extra == 'excel' + - xlrd>=2.0.1 ; extra == 'excel' + - xlsxwriter>=3.0.5 ; extra == 'excel' + - pyarrow>=10.0.1 ; extra == 'parquet' + - pyarrow>=10.0.1 ; extra == 'feather' + - tables>=3.8.0 ; extra == 'hdf5' + - pyreadstat>=1.2.0 ; extra == 'spss' + - sqlalchemy>=2.0.0 ; extra == 'postgresql' + - psycopg2>=2.9.6 ; extra == 'postgresql' + - adbc-driver-postgresql>=0.8.0 ; extra == 'postgresql' + - sqlalchemy>=2.0.0 ; extra == 'mysql' + - pymysql>=1.0.2 ; extra == 'mysql' + - sqlalchemy>=2.0.0 ; extra == 'sql-other' + - adbc-driver-postgresql>=0.8.0 ; extra == 'sql-other' + - adbc-driver-sqlite>=0.8.0 ; extra == 'sql-other' + - beautifulsoup4>=4.11.2 ; extra == 'html' + - html5lib>=1.1 ; extra == 'html' + - lxml>=4.9.2 ; extra == 'html' + - lxml>=4.9.2 ; extra == 'xml' + - matplotlib>=3.6.3 ; extra == 'plot' + - jinja2>=3.1.2 ; extra == 'output-formatting' + - tabulate>=0.9.0 ; extra == 'output-formatting' + - pyqt5>=5.15.9 ; extra == 'clipboard' + - qtpy>=2.3.0 ; extra == 'clipboard' + - zstandard>=0.19.0 ; extra == 'compression' + - dataframe-api-compat>=0.1.7 ; extra == 'consortium-standard' + - adbc-driver-postgresql>=0.8.0 ; extra == 'all' + - adbc-driver-sqlite>=0.8.0 ; extra == 'all' + - beautifulsoup4>=4.11.2 ; extra == 'all' + - bottleneck>=1.3.6 ; extra == 'all' + - dataframe-api-compat>=0.1.7 ; extra == 'all' + - fastparquet>=2022.12.0 ; extra == 'all' + - fsspec>=2022.11.0 ; extra == 'all' + - gcsfs>=2022.11.0 ; extra == 'all' + - html5lib>=1.1 ; extra == 'all' + - hypothesis>=6.46.1 ; extra == 'all' + - jinja2>=3.1.2 ; extra == 'all' + - lxml>=4.9.2 ; extra == 'all' + - matplotlib>=3.6.3 ; extra == 'all' + - numba>=0.56.4 ; extra == 'all' + - numexpr>=2.8.4 ; extra == 'all' + - odfpy>=1.4.1 ; extra == 'all' + - openpyxl>=3.1.0 ; extra == 'all' + - pandas-gbq>=0.19.0 ; extra == 'all' + - psycopg2>=2.9.6 ; extra == 'all' + - pyarrow>=10.0.1 ; extra == 'all' + - pymysql>=1.0.2 ; extra == 'all' + - pyqt5>=5.15.9 ; extra == 'all' + - pyreadstat>=1.2.0 ; extra == 'all' + - pytest>=7.3.2 ; extra == 'all' + - pytest-xdist>=2.2.0 ; extra == 'all' + - python-calamine>=0.1.7 ; extra == 'all' + - pyxlsb>=1.0.10 ; extra == 'all' + - qtpy>=2.3.0 ; extra == 'all' + - scipy>=1.10.0 ; extra == 'all' + - s3fs>=2022.11.0 ; extra == 'all' + - sqlalchemy>=2.0.0 ; extra == 'all' + - tables>=3.8.0 ; extra == 'all' + - tabulate>=0.9.0 ; extra == 'all' + - xarray>=2022.12.0 ; extra == 'all' + - xlrd>=2.0.1 ; extra == 'all' + - xlsxwriter>=3.0.5 ; extra == 'all' + - zstandard>=0.19.0 ; extra == 'all' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/40/a8/4dac1f8f8235e5d25b9955d02ff6f29396191d4e665d71122c3722ca83c5/pandas-2.3.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl + name: pandas + version: 2.3.3 + sha256: dd7478f1463441ae4ca7308a70e90b33470fa593429f9d4c578dd00d1fa78838 + requires_dist: + - numpy>=1.22.4 ; python_full_version < '3.11' + - numpy>=1.23.2 ; python_full_version == '3.11.*' + - numpy>=1.26.0 ; python_full_version >= '3.12' + - python-dateutil>=2.8.2 + - pytz>=2020.1 + - tzdata>=2022.7 + - hypothesis>=6.46.1 ; extra == 'test' + - pytest>=7.3.2 ; extra == 'test' + - pytest-xdist>=2.2.0 ; extra == 'test' + - pyarrow>=10.0.1 ; extra == 'pyarrow' + - bottleneck>=1.3.6 ; extra == 'performance' + - numba>=0.56.4 ; extra == 'performance' + - numexpr>=2.8.4 ; extra == 'performance' + - scipy>=1.10.0 ; extra == 'computation' + - xarray>=2022.12.0 ; extra == 'computation' + - fsspec>=2022.11.0 ; extra == 'fss' + - s3fs>=2022.11.0 ; extra == 'aws' + - gcsfs>=2022.11.0 ; extra == 'gcp' + - pandas-gbq>=0.19.0 ; extra == 'gcp' + - odfpy>=1.4.1 ; extra == 'excel' + - openpyxl>=3.1.0 ; extra == 'excel' + - python-calamine>=0.1.7 ; extra == 'excel' + - pyxlsb>=1.0.10 ; extra == 'excel' + - xlrd>=2.0.1 ; extra == 'excel' + - xlsxwriter>=3.0.5 ; extra == 'excel' + - pyarrow>=10.0.1 ; extra == 'parquet' + - pyarrow>=10.0.1 ; extra == 'feather' + - tables>=3.8.0 ; extra == 'hdf5' + - pyreadstat>=1.2.0 ; extra == 'spss' + - sqlalchemy>=2.0.0 ; extra == 'postgresql' + - psycopg2>=2.9.6 ; extra == 'postgresql' + - adbc-driver-postgresql>=0.8.0 ; extra == 'postgresql' + - sqlalchemy>=2.0.0 ; extra == 'mysql' + - pymysql>=1.0.2 ; extra == 'mysql' + - sqlalchemy>=2.0.0 ; extra == 'sql-other' + - adbc-driver-postgresql>=0.8.0 ; extra == 'sql-other' + - adbc-driver-sqlite>=0.8.0 ; extra == 'sql-other' + - beautifulsoup4>=4.11.2 ; extra == 'html' + - html5lib>=1.1 ; extra == 'html' + - lxml>=4.9.2 ; extra == 'html' + - lxml>=4.9.2 ; extra == 'xml' + - matplotlib>=3.6.3 ; extra == 'plot' + - jinja2>=3.1.2 ; extra == 'output-formatting' + - tabulate>=0.9.0 ; extra == 'output-formatting' + - pyqt5>=5.15.9 ; extra == 'clipboard' + - qtpy>=2.3.0 ; extra == 'clipboard' + - zstandard>=0.19.0 ; extra == 'compression' + - dataframe-api-compat>=0.1.7 ; extra == 'consortium-standard' + - adbc-driver-postgresql>=0.8.0 ; extra == 'all' + - adbc-driver-sqlite>=0.8.0 ; extra == 'all' + - beautifulsoup4>=4.11.2 ; extra == 'all' + - bottleneck>=1.3.6 ; extra == 'all' + - dataframe-api-compat>=0.1.7 ; extra == 'all' + - fastparquet>=2022.12.0 ; extra == 'all' + - fsspec>=2022.11.0 ; extra == 'all' + - gcsfs>=2022.11.0 ; extra == 'all' + - html5lib>=1.1 ; extra == 'all' + - hypothesis>=6.46.1 ; extra == 'all' + - jinja2>=3.1.2 ; extra == 'all' + - lxml>=4.9.2 ; extra == 'all' + - matplotlib>=3.6.3 ; extra == 'all' + - numba>=0.56.4 ; extra == 'all' + - numexpr>=2.8.4 ; extra == 'all' + - odfpy>=1.4.1 ; extra == 'all' + - openpyxl>=3.1.0 ; extra == 'all' + - pandas-gbq>=0.19.0 ; extra == 'all' + - psycopg2>=2.9.6 ; extra == 'all' + - pyarrow>=10.0.1 ; extra == 'all' + - pymysql>=1.0.2 ; extra == 'all' + - pyqt5>=5.15.9 ; extra == 'all' + - pyreadstat>=1.2.0 ; extra == 'all' + - pytest>=7.3.2 ; extra == 'all' + - pytest-xdist>=2.2.0 ; extra == 'all' + - python-calamine>=0.1.7 ; extra == 'all' + - pyxlsb>=1.0.10 ; extra == 'all' + - qtpy>=2.3.0 ; extra == 'all' + - scipy>=1.10.0 ; extra == 'all' + - s3fs>=2022.11.0 ; extra == 'all' + - sqlalchemy>=2.0.0 ; extra == 'all' + - tables>=3.8.0 ; extra == 'all' + - tabulate>=0.9.0 ; extra == 'all' + - xarray>=2022.12.0 ; extra == 'all' + - xlrd>=2.0.1 ; extra == 'all' + - xlsxwriter>=3.0.5 ; extra == 'all' + - zstandard>=0.19.0 ; extra == 'all' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl + name: pandocfilters + version: 1.5.1 + sha256: 93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc + requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*' +- pypi: https://files.pythonhosted.org/packages/58/85/3290cc84bb35503293ea23d2a0b39a78cf02c560ae1455502b042975c951/panel-1.8.10-py3-none-any.whl + name: panel + version: 1.8.10 + sha256: b1de9304e729b87fdeee59a5be5b0d3ce7786dc301626cb728f69fbc4c136693 + requires_dist: - bleach - - bokeh >=3.5.0,<3.7.0 + - bokeh>=3.7.0,<3.10.0 - linkify-it-py - markdown - markdown-it-py - mdit-py-plugins + - narwhals>=2 - packaging - - pandas >=1.2 - - param >=2.1.0,<3.0 - - python >=3.10 - - pyviz_comms >=2.0.0 + - pandas>=1.2 + - param>=2.1.0,<3.0 + - pyviz-comms>=2.0.0 - requests - tqdm - - typing_extensions - constrains: - - holoviews >=1.18.0 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/panel?source=hash-mapping - size: 21525119 - timestamp: 1739557836573 -- conda: https://conda.anaconda.org/conda-forge/noarch/param-2.2.0-pyhd8ed1ab_0.conda - sha256: 857c0e09b51d5c81d5a2144d4a5bd3dc15f81a52f1bf3da9290baff3deae6b5d - md5: 8bd46aebe85bd9c5f30affd520ab441f - depends: - - python >=3.9 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/param?source=hash-mapping - size: 104754 - timestamp: 1734441144421 -- conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.4-pyhd8ed1ab_1.conda - sha256: 17131120c10401a99205fc6fe436e7903c0fa092f1b3e80452927ab377239bcc - md5: 5c092057b6badd30f75b06244ecd01c9 - depends: - - python >=3.9 - license: MIT - license_family: MIT - purls: - - pkg:pypi/parso?source=hash-mapping - size: 75295 - timestamp: 1733271352153 -- conda: https://conda.anaconda.org/conda-forge/noarch/pastel-0.2.1-pyhd8ed1ab_0.tar.bz2 - sha256: 9153f0f38c76a09da7688a61fdbf8f3d7504e2326bef53e4ec20d994311b15bd - md5: a4eea5bff523f26442405bc5d1f52adb - depends: - - python >=2.7 - license: MIT - license_family: MIT - purls: - - pkg:pypi/pastel?source=hash-mapping - size: 9982 - timestamp: 1640899127851 -- conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-0.12.1-pyhd8ed1ab_1.conda - sha256: 9f64009cdf5b8e529995f18e03665b03f5d07c0b17445b8badef45bde76249ee - md5: 617f15191456cc6a13db418a275435e5 - depends: - - python >=3.9 - license: MPL-2.0 - license_family: MOZILLA - purls: - - pkg:pypi/pathspec?source=hash-mapping - size: 41075 - timestamp: 1733233471940 -- conda: https://conda.anaconda.org/conda-forge/linux-64/pcre-8.45-h9c3ff4c_0.tar.bz2 - sha256: 8f35c244b1631a4f31fb1d66ab6e1d9bfac0ca9b679deced1112c7225b3ad138 - md5: c05d1820a6d34ff07aaaab7a9b7eddaa - depends: - - libgcc-ng >=9.3.0 - - libstdcxx-ng >=9.3.0 - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 259377 - timestamp: 1623788789327 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pcre-8.45-h01db608_0.tar.bz2 - sha256: 7a6062de76f695f6d8f0ddda0ff171e4b47e2b863d6012def440c7703aea0069 - md5: 3963d9f84749d6cdba1f12c65967ccd1 - depends: - - libgcc-ng >=9.3.0 - - libstdcxx-ng >=9.3.0 - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 249883 - timestamp: 1623793306266 + - typing-extensions + - watchfiles ; extra == 'dev' + - bokeh-fastapi>=0.1.5,<0.2.0 ; extra == 'fastapi' + - fastapi[standard] ; extra == 'fastapi' + - mypy ; extra == 'mypy' + - pandas-stubs ; extra == 'mypy' + - types-bleach ; extra == 'mypy' + - types-croniter ; extra == 'mypy' + - types-markdown ; extra == 'mypy' + - types-psutil ; extra == 'mypy' + - types-requests ; extra == 'mypy' + - types-tqdm ; extra == 'mypy' + - typing-extensions ; extra == 'mypy' + - holoviews>=1.18.0 ; extra == 'recommended' + - jupyterlab ; extra == 'recommended' + - matplotlib ; extra == 'recommended' + - pillow ; extra == 'recommended' + - plotly ; extra == 'recommended' + - psutil ; extra == 'tests' + - pytest ; extra == 'tests' + - pytest-asyncio ; extra == 'tests' + - pytest-rerunfailures<16.0 ; extra == 'tests' + - pytest-xdist ; extra == 'tests' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/ea/2b/4bc14b04ae260e68c4db7d1e5d2fdda214fb4381236fc8e2d720ed14e2d9/param-2.3.3-py3-none-any.whl + name: param + version: 2.3.3 + sha256: ae25afbc372c1a5e4ee72935cc76ff77b1db1c4eb092bc46715c906ef3cc2e69 + requires_dist: + - aiohttp ; extra == 'all' + - cloudpickle ; extra == 'all' + - gmpy2 ; extra == 'all' + - ipython ; extra == 'all' + - jsonschema ; extra == 'all' + - nbval ; extra == 'all' + - nest-asyncio ; extra == 'all' + - numpy ; extra == 'all' + - odfpy ; extra == 'all' + - openpyxl ; extra == 'all' + - pandas ; extra == 'all' + - panel ; extra == 'all' + - pyarrow ; extra == 'all' + - pytest ; extra == 'all' + - pytest-asyncio ; extra == 'all' + - pytest-cov ; extra == 'all' + - pytest-xdist ; extra == 'all' + - tables ; extra == 'all' + - xlrd ; extra == 'all' + - aiohttp ; extra == 'examples' + - pandas ; extra == 'examples' + - panel ; extra == 'examples' + - pytest ; extra == 'tests' + - pytest-asyncio ; extra == 'tests' + - pytest-cov ; extra == 'tests' + - odfpy ; extra == 'tests-deser' + - openpyxl ; extra == 'tests-deser' + - pyarrow ; extra == 'tests-deser' + - tables ; extra == 'tests-deser' + - xlrd ; extra == 'tests-deser' + - aiohttp ; extra == 'tests-examples' + - nbval ; extra == 'tests-examples' + - pandas ; extra == 'tests-examples' + - panel ; extra == 'tests-examples' + - pytest ; extra == 'tests-examples' + - pytest-asyncio ; extra == 'tests-examples' + - pytest-xdist ; extra == 'tests-examples' + - aiohttp ; extra == 'tests-full' + - cloudpickle ; extra == 'tests-full' + - gmpy2 ; extra == 'tests-full' + - ipython ; extra == 'tests-full' + - jsonschema ; extra == 'tests-full' + - nbval ; extra == 'tests-full' + - nest-asyncio ; extra == 'tests-full' + - numpy ; extra == 'tests-full' + - odfpy ; extra == 'tests-full' + - openpyxl ; extra == 'tests-full' + - pandas ; extra == 'tests-full' + - panel ; extra == 'tests-full' + - pyarrow ; extra == 'tests-full' + - pytest ; extra == 'tests-full' + - pytest-asyncio ; extra == 'tests-full' + - pytest-cov ; extra == 'tests-full' + - pytest-xdist ; extra == 'tests-full' + - tables ; extra == 'tests-full' + - xlrd ; extra == 'tests-full' + - cloudpickle ; extra == 'tests-pypy' + - ipython ; extra == 'tests-pypy' + - jsonschema ; extra == 'tests-pypy' + - nest-asyncio ; extra == 'tests-pypy' + - numpy ; extra == 'tests-pypy' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/99/5d/8268b644392ee874ee82a635cd0df1773de230bde356c38de28e298392cc/parso-0.8.7-py2.py3-none-any.whl + name: parso + version: 0.8.7 + sha256: a8926eb2a1b915486941fdbd31e86a4baf88fe8c210f25f2f35ecec5b574ca1c + requires_dist: + - flake8==5.0.4 ; extra == 'qa' + - types-setuptools==67.2.0.1 ; extra == 'qa' + - zuban==0.5.1 ; extra == 'qa' + - docopt ; extra == 'testing' + - pytest ; extra == 'testing' + requires_python: '>=3.6' +- pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl + name: pathspec + version: 1.1.1 + sha256: a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189 + requires_dist: + - hyperscan>=0.7 ; extra == 'hyperscan' + - typing-extensions>=4 ; extra == 'optional' + - google-re2>=1.1 ; extra == 're2' + requires_python: '>=3.9' - conda: https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.47-haa7fec5_0.conda sha256: 5e6f7d161356fefd981948bea5139c5aa0436767751a6930cb1ca801ebb113ff md5: 7a3bff861a6583f1889021facefc08b1 @@ -4778,82 +4203,85 @@ packages: purls: [] size: 13338804 timestamp: 1703310557094 -- conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda - sha256: 202af1de83b585d36445dc1fda94266697341994d1a3328fabde4989e1b3d07a - md5: d0d408b1f18883a944376da5cf8101ea - depends: - - ptyprocess >=0.5 - - python >=3.9 - license: ISC - purls: - - pkg:pypi/pexpect?source=compressed-mapping - size: 53561 - timestamp: 1733302019362 -- conda: https://conda.anaconda.org/conda-forge/noarch/pickleshare-0.7.5-pyhd8ed1ab_1004.conda - sha256: e2ac3d66c367dada209fc6da43e645672364b9fd5f9d28b9f016e24b81af475b - md5: 11a9d1d09a3615fc07c3faf79bc0b943 - depends: - - python >=3.9 - license: MIT - license_family: MIT - purls: - - pkg:pypi/pickleshare?source=hash-mapping - size: 11748 - timestamp: 1733327448200 -- conda: https://conda.anaconda.org/conda-forge/linux-64/pillow-11.1.0-py310h7e6dc6c_0.conda - sha256: e11d694b7c12b6a76624e8c3e48892924668a97ec26f353ce37b0648bd12ad87 - md5: 14d300b9e1504748e70cc6499a7b4d25 - depends: - - __glibc >=2.17,<3.0.a0 - - freetype >=2.12.1,<3.0a0 - - lcms2 >=2.16,<3.0a0 - - libgcc >=13 - - libjpeg-turbo >=3.0.0,<4.0a0 - - libtiff >=4.7.0,<4.8.0a0 - - libwebp-base >=1.5.0,<2.0a0 - - libxcb >=1.17.0,<2.0a0 - - libzlib >=1.3.1,<2.0a0 - - openjpeg >=2.5.3,<3.0a0 - - python >=3.10,<3.11.0a0 - - python_abi 3.10.* *_cp310 - - tk >=8.6.13,<8.7.0a0 - license: HPND - purls: - - pkg:pypi/pillow?source=hash-mapping - size: 42419230 - timestamp: 1735929858736 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pillow-11.1.0-py310h34c99de_0.conda - sha256: ae9a9c5051cb7a25f3163952c4d3cdd547f5feadccad9d55c7e5358f7caf78a9 - md5: c4fa80647a708505d65573c2353bc216 - depends: - - freetype >=2.12.1,<3.0a0 - - lcms2 >=2.16,<3.0a0 - - libgcc >=13 - - libjpeg-turbo >=3.0.0,<4.0a0 - - libtiff >=4.7.0,<4.8.0a0 - - libwebp-base >=1.5.0,<2.0a0 - - libxcb >=1.17.0,<2.0a0 - - libzlib >=1.3.1,<2.0a0 - - openjpeg >=2.5.3,<3.0a0 - - python >=3.10,<3.11.0a0 - - python_abi 3.10.* *_cp310 - - tk >=8.6.13,<8.7.0a0 - license: HPND - purls: - - pkg:pypi/pillow?source=hash-mapping - size: 41918038 - timestamp: 1735932078553 -- conda: https://conda.anaconda.org/conda-forge/noarch/pkginfo-1.12.1.2-pyhd8ed1ab_0.conda - sha256: 353fd5a2c3ce31811a6272cd328874eb0d327b1eafd32a1e19001c4ad137ad3a - md5: dc702b2fae7ebe770aff3c83adb16b63 - depends: - - python >=3.9 - license: MIT - license_family: MIT - purls: - - pkg:pypi/pkginfo?source=hash-mapping - size: 30536 - timestamp: 1739984682585 +- pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl + name: pexpect + version: 4.9.0 + sha256: 7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523 + requires_dist: + - ptyprocess>=0.5 +- pypi: https://files.pythonhosted.org/packages/a1/2a/8c79d6a53169937784604a8ae8d77e45888c41537f7f6f65ed1f407fe66d/pillow-12.2.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl + name: pillow + version: 12.2.0 + sha256: bd9c0c7a0c681a347b3194c500cb1e6ca9cab053ea4d82a5cf45b6b754560136 + requires_dist: + - furo ; extra == 'docs' + - olefile ; extra == 'docs' + - sphinx>=8.2 ; extra == 'docs' + - sphinx-autobuild ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - sphinx-inline-tabs ; extra == 'docs' + - sphinxext-opengraph ; extra == 'docs' + - olefile ; extra == 'fpx' + - olefile ; extra == 'mic' + - arro3-compute ; extra == 'test-arrow' + - arro3-core ; extra == 'test-arrow' + - nanoarrow ; extra == 'test-arrow' + - pyarrow ; extra == 'test-arrow' + - check-manifest ; extra == 'tests' + - coverage>=7.4.2 ; extra == 'tests' + - defusedxml ; extra == 'tests' + - markdown2 ; extra == 'tests' + - olefile ; extra == 'tests' + - packaging ; extra == 'tests' + - pyroma>=5 ; extra == 'tests' + - pytest ; extra == 'tests' + - pytest-cov ; extra == 'tests' + - pytest-timeout ; extra == 'tests' + - pytest-xdist ; extra == 'tests' + - trove-classifiers>=2024.10.12 ; extra == 'tests' + - defusedxml ; extra == 'xmp' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/b5/42/bbcb6051030e1e421d103ce7a8ecadf837aa2f39b8f82ef1a8d37c3d4ebc/pillow-12.2.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + name: pillow + version: 12.2.0 + sha256: 88d387ff40b3ff7c274947ed3125dedf5262ec6919d83946753b5f3d7c67ea4c + requires_dist: + - furo ; extra == 'docs' + - olefile ; extra == 'docs' + - sphinx>=8.2 ; extra == 'docs' + - sphinx-autobuild ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - sphinx-inline-tabs ; extra == 'docs' + - sphinxext-opengraph ; extra == 'docs' + - olefile ; extra == 'fpx' + - olefile ; extra == 'mic' + - arro3-compute ; extra == 'test-arrow' + - arro3-core ; extra == 'test-arrow' + - nanoarrow ; extra == 'test-arrow' + - pyarrow ; extra == 'test-arrow' + - check-manifest ; extra == 'tests' + - coverage>=7.4.2 ; extra == 'tests' + - defusedxml ; extra == 'tests' + - markdown2 ; extra == 'tests' + - olefile ; extra == 'tests' + - packaging ; extra == 'tests' + - pyroma>=5 ; extra == 'tests' + - pytest ; extra == 'tests' + - pytest-cov ; extra == 'tests' + - pytest-timeout ; extra == 'tests' + - pytest-xdist ; extra == 'tests' + - trove-classifiers>=2024.10.12 ; extra == 'tests' + - defusedxml ; extra == 'xmp' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/fa/3d/f4f2ba829efb54b6cd2d91349c7463316a9cc55a43fc980447416c88540f/pkginfo-1.12.1.2-py3-none-any.whl + name: pkginfo + version: 1.12.1.2 + sha256: c783ac885519cab2c34927ccfa6bf64b5a704d7c69afaea583dd9b7afe969343 + requires_dist: + - pytest ; extra == 'testing' + - pytest-cov ; extra == 'testing' + - wheel ; extra == 'testing' + requires_python: '>=3.8' - conda: https://conda.anaconda.org/conda-forge/noarch/pkgutil-resolve-name-1.3.10-pyhd8ed1ab_2.conda sha256: adb2dde5b4f7da70ae81309cce6188ed3286ff280355cf1931b45d91164d2ad8 md5: 5a5870a74432aa332f7d32180633ad05 @@ -4864,41 +4292,30 @@ packages: - pkg:pypi/pkgutil-resolve-name?source=hash-mapping size: 10693 timestamp: 1733344619659 -- conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.3.6-pyhd8ed1ab_1.conda - sha256: bb50f6499e8bc1d1a26f17716c97984671121608dc0c3ecd34858112bce59a27 - md5: 577852c7e53901ddccc7e6a9959ddebe - depends: - - python >=3.9 - license: MIT - license_family: MIT - purls: - - pkg:pypi/platformdirs?source=hash-mapping - size: 20448 - timestamp: 1733232756001 -- conda: https://conda.anaconda.org/conda-forge/noarch/plotlydash-tornado-cmd-0.0.6-pyhd8ed1ab_2.conda - sha256: 0af36f971af405a4523d4d30216938051abfb4361f03ffad298063992c89413d - md5: 8d36d0557fdbf64b4efcbf22f52042dd - depends: - - click >=7.0 - - python >=3.9,<4.0.0 - - tornado >=5.0 - license: Apache-2.0 - license_family: APACHE - purls: - - pkg:pypi/plotlydash-tornado-cmd?source=hash-mapping - size: 13553 - timestamp: 1735207860605 -- conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.5.0-pyhd8ed1ab_1.conda - sha256: 122433fc5318816b8c69283aaf267c73d87aa2d09ce39f64c9805c9a3b264819 - md5: e9dcbce5f45f9ee500e728ae58b605b6 - depends: - - python >=3.9 - license: MIT - license_family: MIT - purls: - - pkg:pypi/pluggy?source=hash-mapping - size: 23595 - timestamp: 1733222855563 +- pypi: https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl + name: platformdirs + version: 4.9.6 + sha256: e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/29/1c/1b83ffb56adb76abe46df91439e9a613bba45e4612a4e6a7bbebca0b35bb/plotlydash_tornado_cmd-0.0.6-py3-none-any.whl + name: plotlydash-tornado-cmd + version: 0.0.6 + sha256: a4d4bb490230178dbf3eb1fea0126300a697c3fdf6ff4708a3f4501e173814b5 + requires_dist: + - tornado>=5.0 + - click>=7.0 + requires_python: '>=3.6' +- pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + name: pluggy + version: 1.6.0 + sha256: e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 + requires_dist: + - pre-commit ; extra == 'dev' + - tox ; extra == 'dev' + - pytest ; extra == 'testing' + - pytest-benchmark ; extra == 'testing' + - coverage ; extra == 'testing' + requires_python: '>=3.9' - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.21.1-pyhd8ed1ab_0.conda sha256: bc8f00d5155deb7b47702cb8370f233935704100dbc23e30747c161d1b6cf3ab md5: 3e01e386307acc60b2f89af0b2e161aa @@ -4910,30 +4327,13 @@ packages: - pkg:pypi/prometheus-client?source=hash-mapping size: 49002 timestamp: 1733327434163 -- conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.50-pyha770c72_0.conda - sha256: 0749c49a349bf55b8539ce5addce559b77592165da622944a51c630e94d97889 - md5: 7d823138f550b14ecae927a5ff3286de - depends: - - python >=3.9 +- pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl + name: prompt-toolkit + version: 3.0.52 + sha256: 9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955 + requires_dist: - wcwidth - constrains: - - prompt_toolkit 3.0.50 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/prompt-toolkit?source=hash-mapping - size: 271905 - timestamp: 1737453457168 -- conda: https://conda.anaconda.org/conda-forge/noarch/prompt_toolkit-3.0.50-hd8ed1ab_0.conda - sha256: 60504cafe054c307d335bd14163a37a8d611842fba29ee13f88c80863399176a - md5: b5114235809f754b9bff0d14d3d712bc - depends: - - prompt-toolkit >=3.0.50,<3.0.51.0a0 - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 6765 - timestamp: 1737453458406 + requires_python: '>=3.8' - conda: https://conda.anaconda.org/conda-forge/linux-64/propcache-0.2.1-py310h89163eb_1.conda sha256: ed06b08001335e1959d56c25a1c31871df0a56206d4c64b7309d015682dca08f md5: ff4090c5ecf2e74e011c7c2404090ac5 @@ -4990,48 +4390,16 @@ packages: - pkg:pypi/psutil?source=hash-mapping size: 356360 timestamp: 1740663310611 -- conda: https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-hb9d3cd8_1002.conda - sha256: 9c88f8c64590e9567c6c80823f0328e58d3b1efb0e1c539c0315ceca764e0973 - md5: b3c17d95b5a10c6e64a21fa17573e70e - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - license: MIT - license_family: MIT - purls: [] - size: 8252 - timestamp: 1726802366959 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pthread-stubs-0.4-h86ecc28_1002.conda - sha256: 977dfb0cb3935d748521dd80262fe7169ab82920afd38ed14b7fee2ea5ec01ba - md5: bb5a90c93e3bac3d5690acf76b4a6386 - depends: - - libgcc >=13 - license: MIT - license_family: MIT - purls: [] - size: 8342 - timestamp: 1726803319942 -- conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda - sha256: a7713dfe30faf17508ec359e0bc7e0983f5d94682492469bd462cdaae9c64d83 - md5: 7d9daffbb8d8e0af0f769dbbcd173a54 - depends: - - python >=3.9 - license: ISC - purls: - - pkg:pypi/ptyprocess?source=hash-mapping - size: 19457 - timestamp: 1733302371990 -- conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda - sha256: 71bd24600d14bb171a6321d523486f6a06f855e75e547fa0cb2a0953b02047f0 - md5: 3bfdfb8dbcdc4af1ae3f9a8eb3948f04 - depends: - - python >=3.9 - license: MIT - license_family: MIT - purls: - - pkg:pypi/pure-eval?source=hash-mapping - size: 16668 - timestamp: 1733569518868 +- pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl + name: ptyprocess + version: 0.7.0 + sha256: 4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35 +- pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl + name: pure-eval + version: 0.2.3 + sha256: 1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0 + requires_dist: + - pytest ; extra == 'tests' - conda: https://conda.anaconda.org/conda-forge/noarch/pyasn1-0.6.1-pyhd8ed1ab_2.conda sha256: d06051df66e9ab753683d7423fcef873d78bb0c33bd112c3d5be66d529eddf06 md5: 09bb17ed307ad6ab2fd78d32372fdd4e @@ -5146,41 +4514,28 @@ packages: - pkg:pypi/pydantic-core?source=hash-mapping size: 1507348 timestamp: 1734571930496 -- conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.1-pyhd8ed1ab_0.conda - sha256: 28a3e3161390a9d23bc02b4419448f8d27679d9e2c250e29849e37749c8de86b - md5: 232fb4577b6687b2d503ef8e254270c9 - depends: - - python >=3.9 - license: BSD-2-Clause - license_family: BSD - purls: - - pkg:pypi/pygments?source=hash-mapping - size: 888600 - timestamp: 1736243563082 -- conda: https://conda.anaconda.org/conda-forge/noarch/pyjwt-2.9.0-pyhd8ed1ab_1.conda - sha256: b6f47cd0737cb1f5aca10be771641466ec1a3be585382d44877140eb2cb2dd46 - md5: 5ba575830ec18d5c51c59f403310e2c7 +- pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl + name: pygments + version: 2.20.0 + sha256: 81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 + requires_dist: + - colorama>=0.4.6 ; extra == 'windows-terminal' + requires_python: '>=3.9' +- conda: https://conda.anaconda.org/conda-forge/noarch/pyjwt-2.12.1-pyhcf101f3_0.conda + sha256: 4279ee4cf2533fd17910ae7373159d9bee2492d8c50932ddc74dd27a70b15de4 + md5: b27a9f4eca2925036e43542488d3a804 depends: - - python >=3.8 + - python >=3.10 + - typing_extensions >=4.0 + - python constrains: - cryptography >=3.4.0 license: MIT license_family: MIT purls: - pkg:pypi/pyjwt?source=hash-mapping - size: 24346 - timestamp: 1722701382367 -- conda: https://conda.anaconda.org/conda-forge/noarch/pylev-1.4.0-pyhd8ed1ab_0.tar.bz2 - sha256: 50bd91767686bfe769e50a5a1b883e238d944a6163fea43e7c0beaac54ca674f - md5: edf8651c4379d9d1495ad6229622d150 - depends: - - python >=3.3 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/pylev?source=hash-mapping - size: 9270 - timestamp: 1641226468484 + size: 32247 + timestamp: 1773482160904 - conda: https://conda.anaconda.org/conda-forge/noarch/pyopenssl-25.0.0-pyhd8ed1ab_0.conda sha256: 18a487af2ae5e2c380a8bb3fe38da2b4dc3aa8d033aa75202442e1075e6f635b md5: 195fbabc5cc805f2cc10cb881a19cf8b @@ -5194,6 +4549,11 @@ packages: - pkg:pypi/pyopenssl?source=hash-mapping size: 122758 timestamp: 1737243471659 +- pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl + name: pyproject-hooks + version: 1.2.0 + sha256: 9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913 + requires_python: '>=3.7' - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda sha256: ba3b032fa52709ce0d9fd388f63d330a026754587a2f461117cac9ab73d8d0d8 md5: 461219d1a5bd61342293efa2c0c90eac @@ -5271,28 +4631,13 @@ packages: - pkg:pypi/python-dateutil?source=hash-mapping size: 222505 timestamp: 1733215763718 -- conda: https://conda.anaconda.org/conda-forge/noarch/python-dotenv-1.0.1-pyhd8ed1ab_1.conda - sha256: 99713f6b534fef94995c6c16fd21d59f3548784e9111775d692bdc7c44678f02 - md5: e5c6ed218664802d305e79cc2d4491de - depends: - - python >=3.9 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/python-dotenv?source=hash-mapping - size: 24215 - timestamp: 1733243277223 -- conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.1-pyhd8ed1ab_0.conda - sha256: 1b09a28093071c1874862422696429d0d35bd0b8420698003ac004746c5e82a2 - md5: 38e34d2d1d9dca4fb2b9a0a04f604e2c - depends: - - python >=3.9 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/fastjsonschema?source=hash-mapping - size: 226259 - timestamp: 1733236073335 +- pypi: https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl + name: python-dotenv + version: 1.2.2 + sha256: 1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a + requires_dist: + - click>=5.0 ; extra == 'cli' + requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/bd/2d/e94b2f7bab6773c70efc70a61d66e312e1febccd9e0db6b9e0adf58cbad1/python_jose-3.3.0-py2.py3-none-any.whl name: python-jose version: 3.3.0 @@ -5346,29 +4691,11 @@ packages: - pkg:pypi/kubernetes?source=hash-mapping size: 478319 timestamp: 1739952781487 -- conda: https://conda.anaconda.org/conda-forge/noarch/python-libarchive-c-5.2-pyh29332c3_0.conda - sha256: b09d623de9f992b8452524052fb9670675aa6a48515e83ce9e9523bb29d5ffaa - md5: ff2e149fc19d07d5765dd1b56a741681 - depends: - - libarchive - - python >=3.9 - - python - license: CC0-1.0 - purls: - - pkg:pypi/libarchive-c?source=hash-mapping - size: 27381 - timestamp: 1742227113668 -- conda: https://conda.anaconda.org/conda-forge/noarch/python-multipart-0.0.20-pyhff2d567_0.conda - sha256: 1b03678d145b1675b757cba165a0d9803885807792f7eb4495e48a38858c3cca - md5: a28c984e0429aff3ab7386f7de56de6f - depends: - - python >=3.9 - license: Apache-2.0 - license_family: Apache - purls: - - pkg:pypi/python-multipart?source=hash-mapping - size: 27913 - timestamp: 1734420869885 +- pypi: https://files.pythonhosted.org/packages/99/78/4126abcbdbd3c559d43e0db7f7b9173fc6befe45d39a2856cc0b8ec2a5a6/python_multipart-0.0.27-py3-none-any.whl + name: python-multipart + version: 0.0.27 + sha256: 6fccfad17a27334bd0193681b369f476eda3409f17381a2d65aa7df3f7275645 + requires_python: '>=3.10' - conda: https://conda.anaconda.org/conda-forge/noarch/python-slugify-8.0.4-pyhd8ed1ab_1.conda sha256: a84f270426ae7661f79807b107dedb9829c79bd45f77a3033aa021e10556e87f md5: a4059bc12930bddeb41aef71537ffaed @@ -5385,17 +4712,6 @@ packages: - pkg:pypi/python-slugify?source=hash-mapping size: 18991 timestamp: 1733756348165 -- conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.1-pyhd8ed1ab_0.conda - sha256: 1597d6055d34e709ab8915091973552a0b8764c8032ede07c4e99670da029629 - md5: 392c91c42edd569a7ec99ed8648f597a - depends: - - python >=3.9 - license: Apache-2.0 - license_family: APACHE - purls: - - pkg:pypi/tzdata?source=hash-mapping - size: 143794 - timestamp: 1737541204030 - conda: https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.10-5_cp310.conda build_number: 5 sha256: 074d2f0b31f0333b7e553042b17ea54714b74263f8adda9a68a4bd8c7e219971 @@ -5418,17 +4734,10 @@ packages: purls: [] size: 6295 timestamp: 1723823325134 -- conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2024.1-pyhd8ed1ab_0.conda - sha256: 1a7d6b233f7e6e3bbcbad054c8fd51e690a67b129a899a056a5e45dd9f00cb41 - md5: 3eeeeb9e4827ace8c0c1419c85d590ad - depends: - - python >=3.7 - license: MIT - license_family: MIT - purls: - - pkg:pypi/pytz?source=hash-mapping - size: 188538 - timestamp: 1706886944988 +- pypi: https://files.pythonhosted.org/packages/ec/dd/96da98f892250475bdf2328112d7468abdd4acc7b902b6af23f4ed958ea0/pytz-2026.2-py2.py3-none-any.whl + name: pytz + version: '2026.2' + sha256: 04156e608bee23d3792fd45c94ae47fae1036688e75032eea2e3bf0323d1f126 - conda: https://conda.anaconda.org/conda-forge/noarch/pyu2f-0.1.5-pyhd8ed1ab_1.conda sha256: 991caa5408aea018488a2c94e915c11792b9321b0ef64401f4829ebd0abfb3c0 md5: 644bd4ca9f68ef536b902685d773d697 @@ -5441,20 +4750,27 @@ packages: - pkg:pypi/pyu2f?source=hash-mapping size: 36786 timestamp: 1733738704089 -- conda: https://conda.anaconda.org/conda-forge/noarch/pyviz_comms-3.0.4-pyhd8ed1ab_1.conda - sha256: 0352b6935ec73bc996829c61d1ebc7896caa31015073e43036af939fbe91a17a - md5: 99b8cf929b145ae310b333ce3496b56b - depends: +- pypi: https://files.pythonhosted.org/packages/13/5a/f8c0868199bbb231a02616286ce8a4ccb85f5387b9215510297dcfedd214/pyviz_comms-3.0.6-py3-none-any.whl + name: pyviz-comms + version: 3.0.6 + sha256: 4eba6238cd4a7f4add2d11879ce55411785b7d38a7c5dba42c7a0826ca53e6c2 + requires_dist: - param - - python >=3.9 - constrains: - - jupyterlab >=4.0,<5 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/pyviz-comms?source=hash-mapping - size: 48732 - timestamp: 1736890466861 + - flake8 ; extra == 'all' + - jupyterlab~=4.0 ; extra == 'all' + - keyring ; extra == 'all' + - pytest ; extra == 'all' + - rfc3986 ; extra == 'all' + - setuptools>=40.8.0 ; extra == 'all' + - twine ; extra == 'all' + - jupyterlab~=4.0 ; extra == 'build' + - keyring ; extra == 'build' + - rfc3986 ; extra == 'build' + - setuptools>=40.8.0 ; extra == 'build' + - twine ; extra == 'build' + - flake8 ; extra == 'tests' + - pytest ; extra == 'tests' + requires_python: '>=3.8' - conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.2-py310h89163eb_2.conda sha256: 5fba7f5babcac872c72f6509c25331bcfac4f8f5031f0102530a41b41336fce6 md5: fd343408e64cf1e273ab7c710da374db @@ -5485,39 +4801,20 @@ packages: - pkg:pypi/pyyaml?source=hash-mapping size: 174914 timestamp: 1737454839646 -- conda: https://conda.anaconda.org/conda-forge/linux-64/pyzmq-26.3.0-py310h71f11fc_0.conda - sha256: 25c88b22d72a134793d3e294ec1398279cb5eab420d803a3c32e29e1831b2a56 - md5: 930d3ad098bb986315a2f95814c5cf42 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - libsodium >=1.0.20,<1.0.21.0a0 - - libstdcxx >=13 - - python >=3.10,<3.11.0a0 - - python_abi 3.10.* *_cp310 - - zeromq >=4.3.5,<4.4.0a0 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/pyzmq?source=hash-mapping - size: 341820 - timestamp: 1741805357526 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pyzmq-26.3.0-py310h55e1596_0.conda - sha256: 597371dbab3085a8438fab2f8da151df3ecdc9d1efc65ad592e9500efeb79d3e - md5: f66afd2f9d3855e70b3267bae0f2f76a - depends: - - libgcc >=13 - - libsodium >=1.0.20,<1.0.21.0a0 - - libstdcxx >=13 - - python >=3.10,<3.11.0a0 - - python_abi 3.10.* *_cp310 - - zeromq >=4.3.5,<4.4.0a0 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/pyzmq?source=hash-mapping - size: 333294 - timestamp: 1741807072871 +- pypi: https://files.pythonhosted.org/packages/30/76/8f099f9d6482450428b17c4d6b241281af7ce6a9de8149ca8c1c649f6792/pyzmq-27.1.0-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl + name: pyzmq + version: 27.1.0 + sha256: 03ff0b279b40d687691a6217c12242ee71f0fba28bf8626ff50e3ef0f4410e1e + requires_dist: + - cffi ; implementation_name == 'pypy' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/73/78/7d713284dbe022f6440e391bd1f3c48d9185673878034cfb3939cdf333b2/pyzmq-27.1.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl + name: pyzmq + version: 27.1.0 + sha256: bf7b38f9fd7b81cb6d9391b2946382c8237fd814075c6aa9c3b746d53076023b + requires_dist: + - cffi ; implementation_name == 'pypy' + requires_python: '>=3.8' - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda sha256: 2d6d0c026902561ed77cd646b5021aef2d4db22e57a5b0178dfc669231e06d2c md5: 283b96675859b20a825f8fa30f311446 @@ -5584,6 +4881,13 @@ packages: - pkg:pypi/requests-oauthlib?source=hash-mapping size: 25875 timestamp: 1733772348802 +- pypi: https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl + name: requests-toolbelt + version: 1.0.0 + sha256: cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06 + requires_dist: + - requests>=2.0.1,<3.0.0 + requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*' - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda sha256: 2e4372f600490a6e0b3bac60717278448e323cab1c0fecd5f43f7c56535a99c5 md5: 36de09a8d3e5d5e6f4ee63af49e59706 @@ -5607,35 +4911,15 @@ packages: - pkg:pypi/rfc3986-validator?source=hash-mapping size: 7818 timestamp: 1598024297745 -- conda: https://conda.anaconda.org/conda-forge/noarch/rich-13.9.4-pyhd8ed1ab_1.conda - sha256: 06a760c5ae572e72e865d5a87e9fe3cc171e1a9c996e63daf3db52ff1a0b4457 - md5: 7aed65d4ff222bfb7335997aa40b7da5 - depends: - - markdown-it-py >=2.2.0 - - pygments >=2.13.0,<3.0.0 - - python >=3.9 - - typing_extensions >=4.0.0,<5.0.0 - license: MIT - license_family: MIT - purls: - - pkg:pypi/rich?source=hash-mapping - size: 185646 - timestamp: 1733342347277 -- conda: https://conda.anaconda.org/conda-forge/noarch/rich-toolkit-0.11.3-pyh29332c3_0.conda - sha256: e558f8c254a9ff9164d069110da162fc79497d70c60f2c09a5d3d0d7101c5628 - md5: 4ba15ae9388b67d09782798347481f69 - depends: - - python >=3.9 - - rich >=13.7.1 - - click >=8.1.7 - - typing_extensions >=4.12.2 - - python - license: MIT - license_family: MIT - purls: - - pkg:pypi/rich-toolkit?source=hash-mapping - size: 17357 - timestamp: 1733750834072 +- pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl + name: rich + version: 15.0.0 + sha256: 33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb + requires_dist: + - ipywidgets>=7.5.1,<9 ; extra == 'jupyter' + - markdown-it-py>=2.2.0 + - pygments>=2.13.0,<3.0.0 + requires_python: '>=3.9.0' - conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-0.23.1-py310hc1293b2_0.conda sha256: 775f9fe47c18f8c6c4cb706c7837cc04cdc18e6a748fd8964e132d8329975eea md5: 55afda712d4c48108d993ded1bd4de9b @@ -5737,48 +5021,28 @@ packages: - pkg:pypi/ruamel-yaml-clib?source=hash-mapping size: 140150 timestamp: 1728724732658 -- conda: https://conda.anaconda.org/conda-forge/linux-64/secretstorage-3.3.3-py310hff52083_3.conda - sha256: 6e5de234e690eda6bc09cea8db32344539c80e3d35daa7fda2bd9f8c1007532f - md5: 3dcf038a7082c5aee9e6126dd8f2d39a - depends: - - cryptography - - dbus - - jeepney >=0.6 - - python >=3.10,<3.11.0a0 - - python_abi 3.10.* *_cp310 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/secretstorage?source=hash-mapping - size: 27103 - timestamp: 1725915731942 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/secretstorage-3.3.3-py310hbbe02a8_3.conda - sha256: 198295f0690ad7ec73a208fad2d237aec206d3051ed3e71a94bdcf3a8415ebdc - md5: 45ea91c0464d26e5725919b62e22f1c3 - depends: - - cryptography - - dbus - - jeepney >=0.6 - - python >=3.10,<3.11.0a0 - - python_abi 3.10.* *_cp310 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/secretstorage?source=hash-mapping - size: 27386 - timestamp: 1725916584980 -- conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-1.8.3-pyh0d859eb_1.conda - sha256: 00926652bbb8924e265caefdb1db100f86a479e8f1066efe395d5552dde54d02 - md5: 938c8de6b9de091997145b3bf25cdbf9 - depends: - - __linux - - python >=3.9 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/send2trash?source=hash-mapping - size: 22736 - timestamp: 1733322148326 +- pypi: https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl + name: secretstorage + version: 3.5.0 + sha256: 0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137 + requires_dist: + - cryptography>=2.0 + - jeepney>=0.6 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/a6/24/4d91e05817e92e3a61c8a21e08fd0f390f5301f1c448b137c57c4bc6e543/semver-3.0.4-py3-none-any.whl + name: semver + version: 3.0.4 + sha256: 9c824d87ba7f7ab4a1890799cec8596f15c1241cb473404ea1cb0c55e4b04746 + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/1c/78/504fdd027da3b84ff1aecd9f6957e65f35134534ccc6da8628eb71e76d3f/send2trash-2.1.0-py3-none-any.whl + name: send2trash + version: 2.1.0 + sha256: 0da2f112e6d6bb22de6aa6daa7e144831a4febf2a87261451c4ad849fe9a873c + requires_dist: + - pytest>=8 ; extra == 'test' + - pywin32>=305 ; sys_platform == 'win32' and extra == 'nativelib' + - pyobjc>=9.0 ; sys_platform == 'darwin' and extra == 'nativelib' + requires_python: '>=3.8' - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-75.8.2-pyhff2d567_0.conda sha256: 91d664ace7c22e787775069418daa9f232ee8bafdd0a6a080a5ed2395a6fa6b2 md5: 9bddfdbf4e061821a1a443f93223be61 @@ -5790,61 +5054,32 @@ packages: - pkg:pypi/setuptools?source=hash-mapping size: 777736 timestamp: 1740654030775 -- conda: https://conda.anaconda.org/conda-forge/noarch/shellingham-1.5.4-pyhd8ed1ab_1.conda - sha256: 0557c090913aa63cdbe821dbdfa038a321b488e22bc80196c4b3b1aace4914ef - md5: 7c3c2a0f3ebdea2bbc35538d162b43bf - depends: - - python >=3.9 - license: MIT - license_family: MIT - purls: - - pkg:pypi/shellingham?source=compressed-mapping - size: 14462 - timestamp: 1733301007770 +- pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl + name: shellingham + version: 1.5.4 + sha256: 7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686 + requires_python: '>=3.7' - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhd8ed1ab_0.conda sha256: 41db0180680cc67c3fa76544ffd48d6a5679d96f4b71d7498a759e94edc9a2db - md5: a451d576819089b0d672f18768be0f65 - depends: - - python >=3.9 - license: MIT - license_family: MIT - purls: - - pkg:pypi/six?source=hash-mapping - size: 16385 - timestamp: 1733381032766 -- conda: https://conda.anaconda.org/conda-forge/noarch/smmap-5.0.2-pyhd8ed1ab_0.conda - sha256: eb92d0ad94b65af16c73071cc00cc0e10f2532be807beb52758aab2b06eb21e2 - md5: 87f47a78808baf2fa1ea9c315a1e48f1 - depends: - - python >=3.9 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/smmap?source=hash-mapping - size: 26051 - timestamp: 1739781801801 -- conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_1.conda - sha256: c2248418c310bdd1719b186796ae50a8a77ce555228b6acd32768e2543a15012 - md5: bf7a226e58dfb8346c70df36065d86c9 - depends: - - python >=3.9 - license: Apache-2.0 - license_family: Apache - purls: - - pkg:pypi/sniffio?source=hash-mapping - size: 15019 - timestamp: 1733244175724 -- conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.5-pyhd8ed1ab_1.conda - sha256: 54ae221033db8fbcd4998ccb07f3c3828b4d77e73b0c72b18c1d6a507059059c - md5: 3f144b2c34f8cb5a9abd9ed23a39c561 + md5: a451d576819089b0d672f18768be0f65 depends: - - python >=3.8 + - python >=3.9 license: MIT license_family: MIT purls: - - pkg:pypi/soupsieve?source=hash-mapping - size: 36754 - timestamp: 1693929424267 + - pkg:pypi/six?source=hash-mapping + size: 16385 + timestamp: 1733381032766 +- pypi: https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl + name: smmap + version: 5.0.3 + sha256: c106e05d5a61449cf6ba9a1e650227ecfb141590d2a98412103ff35d89fc7b2f + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl + name: soupsieve + version: 2.8.3 + sha256: ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95 + requires_python: '>=3.9' - conda: https://conda.anaconda.org/conda-forge/linux-64/sqlalchemy-1.4.46-py310h1fa729e_0.conda sha256: 168174ea8ed9b45c6f4354dbd3dba8b91eb02904f6fb002954dff4ed3554ba03 md5: 7d70e0b7322c6e9b1f69d72d46af865d @@ -5873,58 +5108,56 @@ packages: - pkg:pypi/sqlalchemy?source=hash-mapping size: 2120099 timestamp: 1672797251665 -- conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda - sha256: 570da295d421661af487f1595045760526964f41471021056e993e73089e9c41 - md5: b1b505328da7a6b246787df4b5a49fbc - depends: - - asttokens - - executing - - pure_eval - - python >=3.9 - license: MIT - license_family: MIT - purls: - - pkg:pypi/stack-data?source=hash-mapping - size: 26988 - timestamp: 1733569565672 -- conda: https://conda.anaconda.org/conda-forge/noarch/starlette-0.46.1-pyha770c72_0.conda - sha256: 821c9e23e9dffaa1519faac109c60f02ebccc6436efd58ea60b85dd7b7f6e9ec - md5: db2f992eed837d11aed1dab97af9e408 - depends: - - anyio >=3.6.2,<5 - - python >=3.9 - - typing_extensions >=3.10.0 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/starlette?source=hash-mapping - size: 58328 - timestamp: 1741450719441 -- conda: https://conda.anaconda.org/conda-forge/noarch/structlog-25.2.0-pyhd8ed1ab_0.conda - sha256: 3ab7773750b97fa26e786de6e40bf7b961323dfd47ebfb3e1b3f8bd5aefedf33 - md5: 96a2a79f23984643b7ba9e3819036e78 - depends: - - python >=3.9 - - typing-extensions - license: Apache-2.0 OR MIT - purls: - - pkg:pypi/structlog?source=hash-mapping - size: 56895 - timestamp: 1741780111760 -- conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh0d859eb_0.conda - sha256: b300557c0382478cf661ddb520263508e4b3b5871b471410450ef2846e8c352c - md5: efba281bbdae5f6b0a1d53c6d4a97c93 - depends: - - __linux - - ptyprocess - - python >=3.8 - - tornado >=6.1.0 - license: BSD-2-Clause - license_family: BSD - purls: - - pkg:pypi/terminado?source=hash-mapping - size: 22452 - timestamp: 1710262728753 +- pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl + name: stack-data + version: 0.6.3 + sha256: d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695 + requires_dist: + - executing>=1.2.0 + - asttokens>=2.1.0 + - pure-eval + - pytest ; extra == 'tests' + - typeguard ; extra == 'tests' + - pygments ; extra == 'tests' + - littleutils ; extra == 'tests' + - cython ; extra == 'tests' +- pypi: https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl + name: starlette + version: 1.0.0 + sha256: d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b + requires_dist: + - anyio>=3.6.2,<5 + - typing-extensions>=4.10.0 ; python_full_version < '3.13' + - httpx>=0.27.0,<0.29.0 ; extra == 'full' + - itsdangerous ; extra == 'full' + - jinja2 ; extra == 'full' + - python-multipart>=0.0.18 ; extra == 'full' + - pyyaml ; extra == 'full' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/a8/45/a132b9074aa18e799b891b91ad72133c98d8042c70f6240e4c5f9dabee2f/structlog-25.5.0-py3-none-any.whl + name: structlog + version: 25.5.0 + sha256: a8453e9b9e636ec59bd9e79bbd4a72f025981b3ba0f5837aebf48f02f37a7f9f + requires_dist: + - typing-extensions ; python_full_version < '3.11' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl + name: terminado + version: 0.18.1 + sha256: a4468e1b37bb318f8a86514f65814e1afc977cf29b3992a4500d9dd305dcceb0 + requires_dist: + - ptyprocess ; os_name != 'nt' + - pywinpty>=1.1.0 ; os_name == 'nt' + - tornado>=6.1.0 + - myst-parser ; extra == 'docs' + - pydata-sphinx-theme ; extra == 'docs' + - sphinx ; extra == 'docs' + - pre-commit ; extra == 'test' + - pytest-timeout ; extra == 'test' + - pytest>=7.0 ; extra == 'test' + - mypy~=1.6 ; extra == 'typing' + - traitlets>=5.11.1 ; extra == 'typing' + requires_python: '>=3.8' - conda: https://conda.anaconda.org/conda-forge/noarch/text-unidecode-1.3-pyhd8ed1ab_2.conda sha256: 4770807cc5a217638c9aea3f05ea55718a82c50f32462df196b5472aff02787f md5: 23b4ba5619c4752976eb7ba1f5acb7e8 @@ -5936,18 +5169,17 @@ packages: - pkg:pypi/text-unidecode?source=hash-mapping size: 65532 timestamp: 1733750024391 -- conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda - sha256: cad582d6f978276522f84bd209a5ddac824742fe2d452af6acf900f8650a73a2 - md5: f1acf5fdefa8300de697982bcb1761c9 - depends: - - python >=3.5 - - webencodings >=0.4 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/tinycss2?source=hash-mapping - size: 28285 - timestamp: 1729802975370 +- pypi: https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl + name: tinycss2 + version: 1.4.0 + sha256: 3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289 + requires_dist: + - webencodings>=0.4 + - sphinx ; extra == 'doc' + - sphinx-rtd-theme ; extra == 'doc' + - pytest ; extra == 'test' + - ruff ; extra == 'test' + requires_python: '>=3.8' - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda sha256: e0569c9caa68bf476bead1bed3d79650bb080b532c64a4af7d8ca286c08dea4e md5: d453b98d9c83e71da0741bb0ff4d76bc @@ -5970,50 +5202,21 @@ packages: purls: [] size: 3351802 timestamp: 1695506242997 -- conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhd8ed1ab_1.conda - sha256: 18636339a79656962723077df9a56c0ac7b8a864329eb8f847ee3d38495b863e - md5: ac944244f1fed2eb49bae07193ae8215 - depends: - - python >=3.9 - license: MIT - license_family: MIT - purls: - - pkg:pypi/tomli?source=hash-mapping - size: 19167 - timestamp: 1733256819729 -- conda: https://conda.anaconda.org/conda-forge/noarch/tomli-w-1.2.0-pyhd8ed1ab_0.conda - sha256: 304834f2438017921d69f05b3f5a6394b42dc89a90a6128a46acbf8160d377f6 - md5: 32e37e8fe9ef45c637ee38ad51377769 - depends: - - python >=3.9 - license: MIT - license_family: MIT - purls: - - pkg:pypi/tomli-w?source=hash-mapping - size: 12680 - timestamp: 1736962345843 -- conda: https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.13.2-pyha770c72_1.conda - sha256: 986fae65f5568e95dbf858d08d77a0f9cca031345a98550f1d4b51d36d8811e2 - md5: 1d9ab4fc875c52db83f9c9b40af4e2c8 - depends: - - python >=3.9 - license: MIT - license_family: MIT - purls: - - pkg:pypi/tomlkit?source=hash-mapping - size: 37372 - timestamp: 1733230836889 -- conda: https://conda.anaconda.org/conda-forge/noarch/toolz-0.12.1-pyhd8ed1ab_0.conda - sha256: 22b0a9790317526e08609d5dfdd828210ae89e6d444a9e954855fc29012e90c6 - md5: 2fcb582444635e2c402e8569bb94e039 - depends: - - python >=3.7 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/toolz?source=hash-mapping - size: 52358 - timestamp: 1706112720607 +- pypi: https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl + name: tomli + version: 2.4.1 + sha256: 0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl + name: tomli-w + version: 1.2.0 + sha256: 188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/b5/11/87d6d29fb5d237229d67973a6c9e06e048f01cf4994dee194ab0ea841814/tomlkit-0.14.0-py3-none-any.whl + name: tomlkit + version: 0.14.0 + sha256: 592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680 + requires_python: '>=3.9' - conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.4.2-py310ha75aee5_0.conda sha256: 9c2b86d4e58c8b0e7d13a7f4c440f34e2201bae9cfc1d7e1d30a5bc7ffb1d4c8 md5: 166d59aab40b9c607b4cc21c03924e9d @@ -6041,17 +5244,23 @@ packages: - pkg:pypi/tornado?source=hash-mapping size: 653906 timestamp: 1732617530355 -- conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.67.1-pyhd8ed1ab_1.conda - sha256: 11e2c85468ae9902d24a27137b6b39b4a78099806e551d390e394a8c34b48e40 - md5: 9efbfdc37242619130ea42b1cc4ed861 - depends: - - colorama - - python >=3.9 - license: MPL-2.0 or MIT - purls: - - pkg:pypi/tqdm?source=hash-mapping - size: 89498 - timestamp: 1735661472632 +- pypi: https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl + name: tqdm + version: 4.67.3 + sha256: ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf + requires_dist: + - colorama ; sys_platform == 'win32' + - importlib-metadata ; python_full_version < '3.8' + - pytest>=6 ; extra == 'dev' + - pytest-cov ; extra == 'dev' + - pytest-timeout ; extra == 'dev' + - pytest-asyncio>=0.24 ; extra == 'dev' + - nbval ; extra == 'dev' + - requests ; extra == 'discord' + - slack-sdk ; extra == 'slack' + - requests ; extra == 'telegram' + - ipywidgets>=6 ; extra == 'notebook' + requires_python: '>=3.7' - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda sha256: f39a5620c6e8e9e98357507262a7869de2ae8cc07da8b7f84e517c9fd6c2b959 md5: 019a7385be9af33791c989871317e1ed @@ -6063,60 +5272,10 @@ packages: - pkg:pypi/traitlets?source=hash-mapping size: 110051 timestamp: 1733367480074 -- conda: https://conda.anaconda.org/conda-forge/noarch/trove-classifiers-2025.3.13.13-pyhd8ed1ab_0.conda - sha256: f4332aecfd16d8b4346ac2814fa8f451976303b96643020578c8a8b20d5f9397 - md5: 381d84cfaa6492e7a358d876edd66dfd - depends: - - python >=3.9 - license: Apache-2.0 - license_family: Apache - purls: - - pkg:pypi/trove-classifiers?source=hash-mapping - size: 18767 - timestamp: 1741887081003 -- conda: https://conda.anaconda.org/conda-forge/noarch/typer-0.15.2-pyhff008b6_0.conda - sha256: fa6eeb42e3bddff74126dd61b01b21a3f4f4791368e93bc5a5775563542b2d4e - md5: 1152565b06e3dc27794c3c11f1050005 - depends: - - typer-slim-standard ==0.15.2 h801b22e_0 - - python >=3.9 - - python - license: MIT - license_family: MIT - purls: - - pkg:pypi/typer?source=hash-mapping - size: 76158 - timestamp: 1740697495168 -- conda: https://conda.anaconda.org/conda-forge/noarch/typer-slim-0.15.2-pyh29332c3_0.conda - sha256: c094713560bfacab0539c863010a5223171d9980cbd419cc799e474ae15aca08 - md5: 7c8d9609e2cfe08dd7672e10fe7e7de9 - depends: - - python >=3.9 - - click >=8.0.0 - - typing_extensions >=3.7.4.3 - - python - constrains: - - typer 0.15.2.* - - rich >=10.11.0 - - shellingham >=1.3.0 - license: MIT - license_family: MIT - purls: - - pkg:pypi/typer-slim?source=hash-mapping - size: 45866 - timestamp: 1740697495167 -- conda: https://conda.anaconda.org/conda-forge/noarch/typer-slim-standard-0.15.2-h801b22e_0.conda - sha256: 79b6b34e90e50e041908939d53053f69285714b0082a0370fba6ab3b38315c8d - md5: ea164fc4e03f61f7ff3c1166001969af - depends: - - typer-slim ==0.15.2 pyh29332c3_0 - - rich - - shellingham - license: MIT - license_family: MIT - purls: [] - size: 5409 - timestamp: 1740697495168 +- pypi: https://files.pythonhosted.org/packages/7b/e3/d81b065a2d866a33a541ac63a2a4cc5737e03ce2379ac3191c98bb8867e3/trove_classifiers-2026.5.7.17-py3-none-any.whl + name: trove-classifiers + version: 2026.5.7.17 + sha256: 5ec0800de5e2ddbd7c663cb4c0c15328f132dc168813897c18866c5c7b93db33 - conda: https://conda.anaconda.org/conda-forge/noarch/types-python-dateutil-2.9.0.20241206-pyhd8ed1ab_0.conda sha256: 8b98cd9464837174ab58aaa912fc95d5831879864676650a383994033533b8d1 md5: 1dbc4a115e2ad9fb7f9d5b68397f66f9 @@ -6138,6 +5297,13 @@ packages: purls: [] size: 10075 timestamp: 1733188758872 +- pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl + name: typing-inspection + version: 0.4.2 + sha256: 4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7 + requires_dist: + - typing-extensions>=4.12.0 + requires_python: '>=3.9' - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.12.2-pyha770c72_1.conda sha256: 337be7af5af8b2817f115b3b68870208b30c31d3439bec07bfb2d8f4823e3568 md5: d17f13df8b65464ca316cbc000a3cb64 @@ -6149,17 +5315,11 @@ packages: - pkg:pypi/typing-extensions?source=hash-mapping size: 39637 timestamp: 1733188758212 -- conda: https://conda.anaconda.org/conda-forge/noarch/typing_utils-0.1.0-pyhd8ed1ab_1.conda - sha256: 3088d5d873411a56bf988eee774559335749aed6f6c28e07bf933256afb9eb6c - md5: f6d7aa696c67756a650e91e15e88223c - depends: - - python >=3.9 - license: Apache-2.0 - license_family: APACHE - purls: - - pkg:pypi/typing-utils?source=hash-mapping - size: 15183 - timestamp: 1733331395943 +- pypi: https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl + name: tzdata + version: '2026.2' + sha256: bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7 + requires_python: '>=2' - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025a-h78e105d_0.conda sha256: c4b1ae8a2931fe9b274c44af29c5475a85b37693999f8c792dad0f8c6734b1de md5: dbcace4706afdfb7eb891f7b37d07c04 @@ -6167,17 +5327,15 @@ packages: purls: [] size: 122921 timestamp: 1737119101255 -- conda: https://conda.anaconda.org/conda-forge/noarch/uc-micro-py-1.0.3-pyhd8ed1ab_1.conda - sha256: a2f837780af450d633efc052219c31378bcad31356766663fb88a99e8e4c817b - md5: 9c96c9876ba45368a03056ddd0f20431 - depends: - - python >=3.9 - license: MIT - license_family: MIT - purls: - - pkg:pypi/uc-micro-py?source=hash-mapping - size: 11199 - timestamp: 1733784280160 +- pypi: https://files.pythonhosted.org/packages/61/73/d21edf5b204d1467e06500080a50f79d49ef2b997c79123a536d4a17d97c/uc_micro_py-2.0.0-py3-none-any.whl + name: uc-micro-py + version: 2.0.0 + sha256: 3603a3859af53e5a39bc7677713c78ea6589ff188d70f4fee165db88e22b242c + requires_dist: + - pytest ; extra == 'test' + - coverage ; extra == 'test' + - pytest-cov ; extra == 'test' + requires_python: '>=3.10' - conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda sha256: e0eb6c8daf892b3056f08416a96d68b0a358b7c46b99c8a50481b22631a4dfc0 md5: e7cb0f5745e4c5035a460248334af7eb @@ -6202,162 +5360,73 @@ packages: - pkg:pypi/urllib3?source=hash-mapping size: 115125 timestamp: 1718728467518 -- conda: https://conda.anaconda.org/conda-forge/noarch/userpath-1.9.2-pyhd8ed1ab_0.conda - sha256: 26e53b42f7fa1127e6115a35b91c20e15f75984648b88f115136f27715d4a440 - md5: 946e3571aaa55e0870fec0dea13de3bf - depends: +- pypi: https://files.pythonhosted.org/packages/43/99/3ec6335ded5b88c2f7ed25c56ffd952546f7ed007ffb1e1539dc3b57015a/userpath-1.9.2-py3-none-any.whl + name: userpath + version: 1.9.2 + sha256: 2cbf01a23d655a1ff8fc166dfb78da1b641d1ceabf0fe5f970767d380b14e89d + requires_dist: - click - - python >=3.9 - license: MIT - license_family: MIT - purls: - - pkg:pypi/userpath?source=hash-mapping - size: 14292 - timestamp: 1735925027874 -- conda: https://conda.anaconda.org/conda-forge/linux-64/uv-0.6.7-h0f3a69f_0.conda - sha256: b8b155bc5912cb34a5465453a3cee76102b94050bb4b20ecf5120f0bf1ac38ff - md5: f9c947d6585a5d484a3f2450916b15fc - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - libstdcxx >=13 - constrains: - - __glibc >=2.17 - license: Apache-2.0 OR MIT - purls: [] - size: 11653248 - timestamp: 1742264952356 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/uv-0.6.7-h2016286_0.conda - sha256: 8096760c7c2ae549763a1e4f1a056657fd56333203050bbd38f66c554559733b - md5: e8a7a92f8cdf8994ca7b2c2874006ba1 - depends: - - libgcc >=13 - - libstdcxx >=13 - constrains: - - __glibc >=2.17 - license: Apache-2.0 OR MIT - purls: [] - size: 10907269 - timestamp: 1742264829930 -- conda: https://conda.anaconda.org/conda-forge/noarch/uvicorn-0.34.0-pyh31011fe_0.conda - sha256: 55c160b0cf9274e2b98bc0f7fcce548bffa8d788bc86aa02801877457040f6fa - md5: 5d448feee86e4740498ec8f8eb40e052 - depends: - - __unix - - click >=7.0 - - h11 >=0.8 - - python >=3.9 - - typing_extensions >=4.0 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/uvicorn?source=hash-mapping - size: 48643 - timestamp: 1734293057914 -- conda: https://conda.anaconda.org/conda-forge/noarch/uvicorn-standard-0.34.0-h31011fe_0.conda - sha256: 87e1531e175e75122f9f37608eb953af4c977465ab0ae11283cc01fef954e4ec - md5: 32a94143a7f65d76d2d5da37dcb4ed79 - depends: - - __unix - - httptools >=0.6.3 - - python-dotenv >=0.13 - - pyyaml >=5.1 - - uvicorn 0.34.0 pyh31011fe_0 - - uvloop >=0.14.0,!=0.15.0,!=0.15.1 - - watchfiles >=0.13 - - websockets >=10.4 - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 7203 - timestamp: 1734293058849 -- conda: https://conda.anaconda.org/conda-forge/linux-64/uvloop-0.21.0-py310ha75aee5_1.conda - sha256: 6aca6dc4d5f4a05569bcf228bbdec7d9ce924efceeb7b7b6c95b07af9c22b317 - md5: 3429ca8351fae2ebf2b898719a7353e2 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - libuv >=1.49.2,<2.0a0 - - python >=3.10,<3.11.0a0 - - python_abi 3.10.* *_cp310 - license: MIT OR Apache-2.0 - purls: - - pkg:pypi/uvloop?source=hash-mapping - size: 671026 - timestamp: 1730214551704 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/uvloop-0.21.0-py310ha766c32_1.conda - sha256: 9acfc8c7c6b39a17c646b1848870aaf5f05522f07b242bb804a20c2e3b3031bc - md5: ba0752d640ebe2426004f8886478a17e - depends: - - libgcc >=13 - - libuv >=1.49.2,<2.0a0 - - python >=3.10,<3.11.0a0 - - python >=3.10,<3.11.0a0 *_cpython - - python_abi 3.10.* *_cp310 - license: MIT OR Apache-2.0 - purls: - - pkg:pypi/uvloop?source=hash-mapping - size: 637525 - timestamp: 1730214659654 -- conda: https://conda.anaconda.org/conda-forge/noarch/virtualenv-20.29.3-pyhd8ed1ab_0.conda - sha256: f7b2cd8ee05769e57dab1f2e2206360cb03d15d4290ddb30442711700c430ba6 - md5: 87a2061465e55be9a997dd8cf8b5a578 - depends: - - distlib >=0.3.7,<1 - - filelock >=3.12.2,<4 - - platformdirs >=3.9.1,<5 - - python >=3.9 - license: MIT - license_family: MIT - purls: - - pkg:pypi/virtualenv?source=hash-mapping - size: 3520880 - timestamp: 1741337922189 -- conda: https://conda.anaconda.org/conda-forge/linux-64/watchfiles-1.0.4-py310h505e2c1_0.conda - sha256: d0c51b58271a10443b57e639ac1d2a39bee17437152905f0a7a8dcf502bd9707 - md5: c684d0977a1f4a42c9d63e24bd1f8423 - depends: - - __glibc >=2.17,<3.0.a0 - - anyio >=3.0.0 - - libgcc >=13 - - python >=3.10,<3.11.0a0 - - python_abi 3.10.* *_cp310 - constrains: - - __glibc >=2.17 - license: MIT - license_family: MIT - purls: - - pkg:pypi/watchfiles?source=hash-mapping - size: 405067 - timestamp: 1736550563496 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/watchfiles-1.0.4-py310h04a307d_0.conda - sha256: 4ba58b8c2a89c587ef3f45aca09cee94f29f8f69ffc8ba4cbf49850e9475afa6 - md5: 72719fa7fd7fce576f38beb42fe56260 - depends: - - anyio >=3.0.0 - - libgcc >=13 - - python >=3.10,<3.11.0a0 - - python >=3.10,<3.11.0a0 *_cpython - - python_abi 3.10.* *_cp310 - constrains: - - __glibc >=2.17 - license: MIT - license_family: MIT - purls: - - pkg:pypi/watchfiles?source=hash-mapping - size: 397481 - timestamp: 1736550641952 -- conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.2.13-pyhd8ed1ab_1.conda - sha256: f21e63e8f7346f9074fd00ca3b079bd3d2fa4d71f1f89d5b6934bf31446dc2a5 - md5: b68980f2495d096e71c7fd9d7ccf63e6 - depends: - - python >=3.9 - license: MIT - license_family: MIT - purls: - - pkg:pypi/wcwidth?source=hash-mapping - size: 32581 - timestamp: 1733231433877 + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/4c/f8/a5d5bac297b1379719050788c6b852c6b3eefcb1e82d8465ed22c10cede7/uv-0.11.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: uv + version: 0.11.11 + sha256: d5b9f31dab557b5ee4257d8c6ba2608a63c7278537cb0cd102cf6fc518e3fb5c + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/ee/d5/f3be167a43192062f1409fd6b857a612665d331174293b4ffc73218872e1/uv-0.11.11-py3-none-manylinux_2_28_aarch64.whl + name: uv + version: 0.11.11 + sha256: 8e8faf2e5b3517155fd18e509b19b21135247d43b7fb9a8d61a44a53118d5ab7 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/31/a3/5b1562db76a5a488274b2332a97199b32d0442aca0ed193697fd47786316/uvicorn-0.46.0-py3-none-any.whl + name: uvicorn + version: 0.46.0 + sha256: bbebbcbed972d162afca128605223022bedd345b7bc7855ce66deb31487a9048 + requires_dist: + - click>=7.0 + - h11>=0.8 + - typing-extensions>=4.0 ; python_full_version < '3.11' + - colorama>=0.4 ; sys_platform == 'win32' and extra == 'standard' + - httptools>=0.6.3 ; extra == 'standard' + - python-dotenv>=0.13 ; extra == 'standard' + - pyyaml>=5.1 ; extra == 'standard' + - uvloop>=0.15.1 ; platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32' and extra == 'standard' + - watchfiles>=0.20 ; extra == 'standard' + - websockets>=10.4 ; extra == 'standard' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/ca/ff/ded57ac5ff40a09e6e198550bab075d780941e0b0f83cbeabd087c59383a/virtualenv-20.33.1-py3-none-any.whl + name: virtualenv + version: 20.33.1 + sha256: 07c19bc66c11acab6a5958b815cbcee30891cd1c2ccf53785a28651a0d8d8a67 + requires_dist: + - distlib>=0.3.7,<1 + - filelock>=3.12.2,<4 + - importlib-metadata>=6.6 ; python_full_version < '3.8' + - platformdirs>=3.9.1,<5 + - furo>=2023.7.26 ; extra == 'docs' + - proselint>=0.13 ; extra == 'docs' + - sphinx>=7.1.2,!=7.3 ; extra == 'docs' + - sphinx-argparse>=0.4 ; extra == 'docs' + - sphinxcontrib-towncrier>=0.2.1a0 ; extra == 'docs' + - towncrier>=23.6 ; extra == 'docs' + - covdefaults>=2.3 ; extra == 'test' + - coverage-enable-subprocess>=1 ; extra == 'test' + - coverage>=7.2.7 ; extra == 'test' + - flaky>=3.7 ; extra == 'test' + - packaging>=23.1 ; extra == 'test' + - pytest-env>=0.8.2 ; extra == 'test' + - pytest-freezer>=0.4.8 ; (python_full_version >= '3.13' and platform_python_implementation == 'CPython' and sys_platform == 'win32' and extra == 'test') or (platform_python_implementation == 'GraalVM' and extra == 'test') or (platform_python_implementation == 'PyPy' and extra == 'test') + - pytest-mock>=3.11.1 ; extra == 'test' + - pytest-randomly>=3.12 ; extra == 'test' + - pytest-timeout>=2.1 ; extra == 'test' + - pytest>=7.4 ; extra == 'test' + - setuptools>=68 ; extra == 'test' + - time-machine>=2.10 ; platform_python_implementation == 'CPython' and extra == 'test' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/41/52/e465037f5375f43533d1a80b6923955201596a99142ed524d77b571a1418/wcwidth-0.7.0-py3-none-any.whl + name: wcwidth + version: 0.7.0 + sha256: 5d69154c429a82910e241c738cd0e2976fac8a2dd47a1a805f4afed1c0f136f2 + requires_python: '>=3.8' - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-24.11.1-pyhd8ed1ab_0.conda sha256: 08315dc2e61766a39219b2d82685fc25a56b2817acf84d5b390176080eaacf99 md5: b49f7b291e15494aafb0a7d74806f337 @@ -6369,17 +5438,10 @@ packages: - pkg:pypi/webcolors?source=hash-mapping size: 18431 timestamp: 1733359823938 -- conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda - sha256: 19ff205e138bb056a46f9e3839935a2e60bd1cf01c8241a5e172a422fed4f9c6 - md5: 2841eb5bfc75ce15e9a0054b98dcd64d - depends: - - python >=3.9 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/webencodings?source=hash-mapping - size: 15496 - timestamp: 1733236131358 +- pypi: https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl + name: webencodings + version: 0.5.1 + sha256: a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.8.0-pyhd8ed1ab_1.conda sha256: 1dd84764424ffc82030c19ad70607e6f9e3b9cb8e633970766d697185652053e md5: 84f8f77f0a9c6ef401ee96611745da8f @@ -6391,34 +5453,6 @@ packages: - pkg:pypi/websocket-client?source=hash-mapping size: 46718 timestamp: 1733157432924 -- conda: https://conda.anaconda.org/conda-forge/linux-64/websockets-15.0.1-py310ha75aee5_0.conda - sha256: b7b71d3a3f05df864270bdb34c1c4d0eb2040ec963accf9e3a23608f419cefd2 - md5: 4e5b66dd5de47375df41ccc432260ac9 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - python >=3.10,<3.11.0a0 - - python_abi 3.10.* *_cp310 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/websockets?source=hash-mapping - size: 207331 - timestamp: 1741285571663 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/websockets-15.0.1-py310ha766c32_0.conda - sha256: 114d08ef3100c8e405f7ccc2d94f2ce8a30315239418fbbdf53c5b4d0924b573 - md5: 8d7f15fc33d6b75887ab6147999c51aa - depends: - - libgcc >=13 - - python >=3.10,<3.11.0a0 - - python >=3.10,<3.11.0a0 *_cpython - - python_abi 3.10.* *_cp310 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/websockets?source=hash-mapping - size: 207257 - timestamp: 1741285634223 - conda: https://conda.anaconda.org/conda-forge/linux-64/wget-1.21.4-hda4d442_0.conda sha256: 70df4ac8cca488618458af4705706551cef7e402bac9c2c41dd17148f60cbd1f md5: 361e96b664eac64a33c20dfd11affbff @@ -6449,70 +5483,16 @@ packages: purls: [] size: 792543 timestamp: 1710770044900 -- conda: https://conda.anaconda.org/conda-forge/noarch/widgetsnbextension-4.0.13-pyhd8ed1ab_1.conda - sha256: a750202ae2a31d8e5ee5a5c127fcc7fa783cd0fbedbc0bf1ab549a109881fa9f - md5: 237db148cc37a466e4222d589029b53e - depends: - - python >=3.9 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/widgetsnbextension?source=hash-mapping - size: 898402 - timestamp: 1733128654300 -- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxau-1.0.12-hb9d3cd8_0.conda - sha256: ed10c9283974d311855ae08a16dfd7e56241fac632aec3b92e3cfe73cff31038 - md5: f6ebe2cb3f82ba6c057dde5d9debe4f7 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - license: MIT - license_family: MIT - purls: [] - size: 14780 - timestamp: 1734229004433 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxau-1.0.12-h86ecc28_0.conda - sha256: 7829a0019b99ba462aece7592d2d7f42e12d12ccd3b9614e529de6ddba453685 - md5: d5397424399a66d33c80b1f2345a36a6 - depends: - - libgcc >=13 - license: MIT - license_family: MIT - purls: [] - size: 15873 - timestamp: 1734230458294 -- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdmcp-1.1.5-hb9d3cd8_0.conda - sha256: 6b250f3e59db07c2514057944a3ea2044d6a8cdde8a47b6497c254520fade1ee - md5: 8035c64cb77ed555e3f150b7b3972480 - depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - license: MIT - license_family: MIT - purls: [] - size: 19901 - timestamp: 1727794976192 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxdmcp-1.1.5-h57736b2_0.conda - sha256: efcc150da5926cf244f757b8376d96a4db78bc15b8d90ca9f56ac6e75755971f - md5: 25a5a7b797fe6e084e04ffe2db02fc62 - depends: - - libgcc >=13 - license: MIT - license_family: MIT - purls: [] - size: 20615 - timestamp: 1727796660574 -- conda: https://conda.anaconda.org/conda-forge/noarch/xyzservices-2025.1.0-pyhd8ed1ab_0.conda - sha256: 9978c22319e85026d5a4134944f73bac820c948ca6b6c32af6b6985b5221cd8a - md5: fdf07e281a9e5e10fc75b2dd444136e9 - depends: - - python >=3.8 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/xyzservices?source=hash-mapping - size: 48641 - timestamp: 1737234992057 +- pypi: https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl + name: widgetsnbextension + version: 4.0.15 + sha256: 8156704e4346a571d9ce73b84bee86a29906c9abfd7223b7228a28899ccf3366 + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/a8/a9/d23012099dc88ec69a29c6407b41d89681cb674c2043cd5b467c7e299c08/xyzservices-2026.3.0-py3-none-any.whl + name: xyzservices + version: 2026.3.0 + sha256: 503183d4b322bfebc3c50cdd21192aa3e81e36c5efbf9133d54ae82143e0576b + requires_python: '>=3.8' - conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h7f98852_2.tar.bz2 sha256: a4e34c710eeb26945bdbdaba82d3d74f60a78f54a874ec10d373811a5d217535 md5: 4cb3ad778ec2d5a7acbdf254eb1c42ae @@ -6567,33 +5547,6 @@ packages: - pkg:pypi/yarl?source=hash-mapping size: 138504 timestamp: 1737576062380 -- conda: https://conda.anaconda.org/conda-forge/linux-64/zeromq-4.3.5-h3b0a872_7.conda - sha256: a4dc72c96848f764bb5a5176aa93dd1e9b9e52804137b99daeebba277b31ea10 - md5: 3947a35e916fcc6b9825449affbf4214 - depends: - - __glibc >=2.17,<3.0.a0 - - krb5 >=1.21.3,<1.22.0a0 - - libgcc >=13 - - libsodium >=1.0.20,<1.0.21.0a0 - - libstdcxx >=13 - license: MPL-2.0 - license_family: MOZILLA - purls: [] - size: 335400 - timestamp: 1731585026517 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/zeromq-4.3.5-h5efb499_7.conda - sha256: a6003096dc0570a86492040ba32b04ce7662b159600be2252b7a0dfb9414e21c - md5: f2f3282559a4b87b7256ecafb4610107 - depends: - - krb5 >=1.21.3,<1.22.0a0 - - libgcc >=13 - - libsodium >=1.0.20,<1.0.21.0a0 - - libstdcxx >=13 - license: MPL-2.0 - license_family: MOZILLA - purls: [] - size: 371419 - timestamp: 1731589490850 - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.21.0-pyhd8ed1ab_1.conda sha256: 567c04f124525c97a096b65769834b7acb047db24b15a56888a322bf3966c3e1 md5: 0c3cc595284c5e8f0f9900a9b228a332 @@ -6628,36 +5581,22 @@ packages: purls: [] size: 95582 timestamp: 1727963203597 -- conda: https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.23.0-py310ha75aee5_1.conda - sha256: 96f96336f76443f5efb05f8a7232cc62f8fff969c27d03aa4aae181745f6f961 - md5: 0316e8d0e00c00631a6de89207db5b09 - depends: - - __glibc >=2.17,<3.0.a0 - - cffi >=1.11 - - libgcc >=13 - - python >=3.10,<3.11.0a0 - - python_abi 3.10.* *_cp310 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/zstandard?source=hash-mapping - size: 720871 - timestamp: 1741853413225 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/zstandard-0.23.0-py310ha766c32_1.conda - sha256: a8021db8b14028f3f8e1db7bc06d651b38484cf0f3df9e5662aa7c279281c9cc - md5: 3da63329ee22ee0ac7d3444e1d222fba - depends: - - cffi >=1.11 - - libgcc >=13 - - python >=3.10,<3.11.0a0 - - python >=3.10,<3.11.0a0 *_cpython - - python_abi 3.10.* *_cp310 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/zstandard?source=hash-mapping - size: 690935 - timestamp: 1741853481115 +- pypi: https://files.pythonhosted.org/packages/73/28/a44bdece01bca027b079f0e00be3b6bd89a4df180071da59a3dd7381665b/zstandard-0.25.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl + name: zstandard + version: 0.25.0 + sha256: e05ab82ea7753354bb054b92e2f288afb750e6b439ff6ca78af52939ebbc476d + requires_dist: + - cffi~=1.17 ; python_full_version < '3.14' and platform_python_implementation != 'PyPy' and extra == 'cffi' + - cffi>=2.0.0b0 ; python_full_version >= '3.14' and platform_python_implementation != 'PyPy' and extra == 'cffi' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/fd/e5/6d36f92a197c3c17729a2125e29c169f460538a7d939a27eaaa6dcfcba8e/zstandard-0.25.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + name: zstandard + version: 0.25.0 + sha256: 4b6d83057e713ff235a12e73916b6d356e3084fd3d14ced499d84240f3eecee0 + requires_dist: + - cffi~=1.17 ; python_full_version < '3.14' and platform_python_implementation != 'PyPy' and extra == 'cffi' + - cffi>=2.0.0b0 ; python_full_version >= '3.14' and platform_python_implementation != 'PyPy' and extra == 'cffi' + requires_python: '>=3.9' - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_1.conda sha256: 532d3623961e34c53aba98db2ad0a33b7a52ff90d6960e505fb2d2efc06bb7da md5: 02e4e2fa41a6528afba2e54cbc4280ff diff --git a/images/jupyterhub/pixi.toml b/images/jupyterhub/pixi.toml index e28aa58..8589f43 100644 --- a/images/jupyterhub/pixi.toml +++ b/images/jupyterhub/pixi.toml @@ -19,8 +19,14 @@ python-kubernetes = "*" kubernetes_asyncio = "==29.0.0" jupyterhub-idle-culler = "==1.2.1" sqlalchemy = "==1.4.46" -jhub-apps = "==2025.11.1" +# jhub-apps#676 requires pyjwt>=2.10. Pin in conda so transitive deps that +# request <2.10 are forced to upgrade rather than pinning to 2.9.0. +pyjwt = ">=2.10,<3" [pypi-dependencies] nebari-jupyterhub-theme = "==2024.7.1" python-keycloak = "==0.26.1" +# Pinned to nebari-dev/jhub-apps#676 (fall-through-on-non-HS256-Bearer) +# until the fix lands in a tagged release. Required for forwardAccessToken: true +# behind Envoy Gateway. +jhub-apps = { git = "https://github.com/nebari-dev/jhub-apps.git", rev = "a94b049a9bcc72e2844d5cf0cc2978d4badd2cd5" } diff --git a/values.yaml b/values.yaml index b459f22..dee391c 100644 --- a/values.yaml +++ b/values.yaml @@ -30,14 +30,19 @@ nebariapp: # enforceAtGateway runs the OIDC filter at Envoy Gateway. Required for # the operator to manage the SecurityPolicy at all. enforceAtGateway: true - # forwardAccessToken would inject the user's fresh access token as - # `Authorization: Bearer` on every upstream request, fixing the - # AccessToken-cookie staleness in Envoy Gateway v1.6. Disabled by default - # because jhub-apps's `get_current_user` reads the Authorization header - # first and tries to decode it as its own HS256 JWT, which fails with - # `InvalidAlgorithmError` and produces an OAuth redirect loop. Re-enable - # once jhub-apps is patched to ignore non-HS256 Authorization tokens. - forwardAccessToken: false + # forwardAccessToken makes Envoy inject the user's freshly-refreshed access + # token as `Authorization: Bearer` on every upstream request. This is the + # only always-current source of the access token: Envoy v1.6 refreshes its + # internal token transparently but does not rotate the AccessToken-* cookie + # content, so cookie-based reads go stale ~5 min after login and break + # downstream token-exchange (jhub-apps env selector, Nebi auto-auth). + # + # Enabled by default: the JupyterHub image bundled by this chart pins + # jhub-apps to the head of nebari-dev/jhub-apps#676, which makes + # `get_current_user` fall through to the jhub-apps cookie when the + # Authorization header is not an HS256 wrapper (the historic redirect-loop + # source). Set to false only if running with an unpatched jhub-apps. + forwardAccessToken: true # landingPage controls whether and how this service appears on the Nebari # landing page (requires nebari-operator with LandingPageConfig support). landingPage: From 3c5d01db6ae327392d8a49fea73931cc6f1470c0 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Fri, 8 May 2026 16:05:34 +0200 Subject: [PATCH 29/60] chore: bump hub image to sha-e906f78 (carries patched jhub-apps) --- values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/values.yaml b/values.yaml index dee391c..5efc900 100644 --- a/values.yaml +++ b/values.yaml @@ -210,7 +210,7 @@ jupyterhub: # Custom Nebari hub image with jhub-apps pre-installed image: name: quay.io/nebari/nebari-data-science-pack-jupyterhub - tag: "sha-8f2e545" + tag: "sha-e906f78" config: JupyterHub: From 8bf06bfba6a8e20d4f063e84689248ccd55ccb07 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Fri, 8 May 2026 16:09:36 +0200 Subject: [PATCH 30/60] chore: pin hub image to PR-53 merge-commit sha-9381aab --- values.yaml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/values.yaml b/values.yaml index 5efc900..9ea1623 100644 --- a/values.yaml +++ b/values.yaml @@ -207,10 +207,13 @@ jupyterhub: # additional_services: [] # startup_apps: [] hub: - # Custom Nebari hub image with jhub-apps pre-installed + # Custom Nebari hub image with jhub-apps pre-installed. + # Tag below is the PR-53 merge-commit SHA at the moment of pinning; the + # corresponding immutable digest is sha256:9e45ad6d85414f316c9028d5dc0d5b28dcc296328df7f4f6a9b602314a1f5bad + # (carries jhub-apps#676 fall-through patch + forwardAccessToken=true). image: name: quay.io/nebari/nebari-data-science-pack-jupyterhub - tag: "sha-e906f78" + tag: "sha-9381aab" config: JupyterHub: From a566ce29250a9aeef4eee5da7dacdc1838ba66d8 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Fri, 8 May 2026 16:26:06 +0200 Subject: [PATCH 31/60] chore: bump jhub-apps git pin to include Starlette 1.0 TemplateResponse fix --- images/jupyterhub/pixi.lock | 6 +++--- images/jupyterhub/pixi.toml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/images/jupyterhub/pixi.lock b/images/jupyterhub/pixi.lock index 1ea2a81..656d490 100644 --- a/images/jupyterhub/pixi.lock +++ b/images/jupyterhub/pixi.lock @@ -202,7 +202,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9a/93/242e2eab5fe682ffcb8b0084bde703a41d51e17ee0f3a31ff0d9d813620a/jedi-0.20.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl - - pypi: git+https://github.com/nebari-dev/jhub-apps.git?rev=a94b049a9bcc72e2844d5cf0cc2978d4badd2cd5#a94b049a9bcc72e2844d5cf0cc2978d4badd2cd5 + - pypi: git+https://github.com/nebari-dev/jhub-apps.git?rev=5d86277f926136e0ecf253d36b261dd456727855#5d86277f926136e0ecf253d36b261dd456727855 - pypi: https://files.pythonhosted.org/packages/b8/42/cf027b4ac873b076189d935b135397675dac80cb29acb13e1ab86ad6c631/json5-0.14.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/38/64/285f20a31679bf547b75602702f7800e74dbabae36ef324f716c02804753/jupyter-1.1.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2d/0b/ceb7694d864abc0a047649aec263878acb9f792e1fec3e676f22dc9015e3/jupyter_client-8.8.0-py3-none-any.whl @@ -484,7 +484,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9a/93/242e2eab5fe682ffcb8b0084bde703a41d51e17ee0f3a31ff0d9d813620a/jedi-0.20.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl - - pypi: git+https://github.com/nebari-dev/jhub-apps.git?rev=a94b049a9bcc72e2844d5cf0cc2978d4badd2cd5#a94b049a9bcc72e2844d5cf0cc2978d4badd2cd5 + - pypi: git+https://github.com/nebari-dev/jhub-apps.git?rev=5d86277f926136e0ecf253d36b261dd456727855#5d86277f926136e0ecf253d36b261dd456727855 - pypi: https://files.pythonhosted.org/packages/b8/42/cf027b4ac873b076189d935b135397675dac80cb29acb13e1ab86ad6c631/json5-0.14.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/38/64/285f20a31679bf547b75602702f7800e74dbabae36ef324f716c02804753/jupyter-1.1.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2d/0b/ceb7694d864abc0a047649aec263878acb9f792e1fec3e676f22dc9015e3/jupyter_client-8.8.0-py3-none-any.whl @@ -2210,7 +2210,7 @@ packages: - async-timeout ; python_full_version < '3.11' and extra == 'test' - trio ; extra == 'trio' requires_python: '>=3.7' -- pypi: git+https://github.com/nebari-dev/jhub-apps.git?rev=a94b049a9bcc72e2844d5cf0cc2978d4badd2cd5#a94b049a9bcc72e2844d5cf0cc2978d4badd2cd5 +- pypi: git+https://github.com/nebari-dev/jhub-apps.git?rev=5d86277f926136e0ecf253d36b261dd456727855#5d86277f926136e0ecf253d36b261dd456727855 name: jhub-apps version: 2025.11.1 requires_dist: diff --git a/images/jupyterhub/pixi.toml b/images/jupyterhub/pixi.toml index 8589f43..93ca5bf 100644 --- a/images/jupyterhub/pixi.toml +++ b/images/jupyterhub/pixi.toml @@ -29,4 +29,4 @@ python-keycloak = "==0.26.1" # Pinned to nebari-dev/jhub-apps#676 (fall-through-on-non-HS256-Bearer) # until the fix lands in a tagged release. Required for forwardAccessToken: true # behind Envoy Gateway. -jhub-apps = { git = "https://github.com/nebari-dev/jhub-apps.git", rev = "a94b049a9bcc72e2844d5cf0cc2978d4badd2cd5" } +jhub-apps = { git = "https://github.com/nebari-dev/jhub-apps.git", rev = "5d86277f926136e0ecf253d36b261dd456727855" } From 179ebd8f86e74adfa25262c64513a38bcf3dc24d Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Fri, 8 May 2026 16:29:57 +0200 Subject: [PATCH 32/60] chore: bump hub image to sha-ab37dda (Starlette 1.0 TemplateResponse fix) --- values.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/values.yaml b/values.yaml index 9ea1623..4f407a8 100644 --- a/values.yaml +++ b/values.yaml @@ -209,11 +209,11 @@ jupyterhub: hub: # Custom Nebari hub image with jhub-apps pre-installed. # Tag below is the PR-53 merge-commit SHA at the moment of pinning; the - # corresponding immutable digest is sha256:9e45ad6d85414f316c9028d5dc0d5b28dcc296328df7f4f6a9b602314a1f5bad - # (carries jhub-apps#676 fall-through patch + forwardAccessToken=true). + # corresponding immutable digest is sha256:33d9d7aece1ef63b88a44f3ee9e23685133576dab95d8ffa95a2ddd2a3af6443 + # (carries jhub-apps#676: fall-through bearer + Starlette 1.0 TemplateResponse fix). image: name: quay.io/nebari/nebari-data-science-pack-jupyterhub - tag: "sha-9381aab" + tag: "sha-ab37dda" config: JupyterHub: From 37eddfcddc9a391921707692cbb25d179d289761 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Thu, 14 May 2026 22:17:09 +0100 Subject: [PATCH 33/60] test(unit): add pytest harness for jupyterhub config modules Loads jupyterhub_config.d files (hyphenated, digit-prefixed) by path via importlib spec. FakeConfig records traitlets-style attribute assignments so tests can assert on what each config module wires onto JupyterHub's `c` global without needing a running hub. Also seeds the harness with .venv-unit/ ignored by git/helm so 'uv venv' can install jupyterhub + oauthenticator for tests without leaking into helm package output. --- .gitignore | 1 + .helmignore | 1 + tests/unit/conftest.py | 45 +++++++ tests/unit/test_keycloak_authenticator.py | 150 ++++++++++++++++++++++ 4 files changed, 197 insertions(+) create mode 100644 tests/unit/conftest.py create mode 100644 tests/unit/test_keycloak_authenticator.py diff --git a/.gitignore b/.gitignore index eb3283b..26390c1 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ venv/ # Pixi .pixi/ +.venv-unit/ diff --git a/.helmignore b/.helmignore index cd857c0..0925765 100644 --- a/.helmignore +++ b/.helmignore @@ -41,3 +41,4 @@ docs/ # Testing tests/ *_test.yaml +.venv-unit/ diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py new file mode 100644 index 0000000..dbfe9f3 --- /dev/null +++ b/tests/unit/conftest.py @@ -0,0 +1,45 @@ +"""Shared fixtures for unit tests against config/jupyterhub/*.py.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path +from types import SimpleNamespace, ModuleType + +REPO_ROOT = Path(__file__).resolve().parents[2] +CONFIG_DIR = REPO_ROOT / "config" / "jupyterhub" + + +def load_config_module(filename: str, inject_c: "FakeConfig | None" = None) -> ModuleType: + """Load a jupyterhub_config.d file (hyphenated, digit-prefixed) by path. + + JupyterHub exec's these files with `c` in scope; if ``inject_c`` is + given, simulate that by exec'ing the file with `c` pre-bound in module + globals. Otherwise the module's `try: c` guard skips the bottom wiring. + """ + path = CONFIG_DIR / filename + mod_name = "_" + path.stem.replace("-", "_") + spec = importlib.util.spec_from_file_location(mod_name, path) + module = importlib.util.module_from_spec(spec) + if inject_c is not None: + module.__dict__["c"] = inject_c + spec.loader.exec_module(module) + return module + + +class FakeTraitlet(SimpleNamespace): + """Stand-in for a traitlets class on the JupyterHub `c` config object. + + Real traitlets validate types; here we just want to record what the + module sets, so SimpleNamespace is enough. + """ + + +class FakeConfig: + """Mimics enough of JupyterHub's `c` to record assignments.""" + + def __getattr__(self, name): + # Attribute access creates a new FakeTraitlet on demand (traitlets-like). + ft = FakeTraitlet() + self.__dict__[name] = ft + return ft diff --git a/tests/unit/test_keycloak_authenticator.py b/tests/unit/test_keycloak_authenticator.py new file mode 100644 index 0000000..0bb070b --- /dev/null +++ b/tests/unit/test_keycloak_authenticator.py @@ -0,0 +1,150 @@ +"""Behaviour of the KeyCloakOAuthenticator wiring in 00-gateway-auth.py.""" + +from __future__ import annotations + +from conftest import FakeConfig, load_config_module + + +ISSUER = "https://kc.example.test/realms/nebari" +CLIENT_ID = "hub" +CLIENT_SECRET = "shhh" +CALLBACK = "https://hub.example.test/hub/oauth_callback" +EXTERNAL = "https://hub.example.test/" + + +def _configure_with_defaults(**overrides): + """Helper: call configure() with sensible defaults; return the FakeConfig.""" + mod = load_config_module("00-gateway-auth.py") + c = FakeConfig() + kwargs = dict( + issuer=ISSUER, + client_id=CLIENT_ID, + client_secret=CLIENT_SECRET, + callback_url=CALLBACK, + external_url=EXTERNAL, + ) + kwargs.update(overrides) + mod.configure(c, **kwargs) + return c, mod + + +# --------------------------------------------------------------------------- +# Cycle 1 — class wiring + URL derivation +# --------------------------------------------------------------------------- + +def test_configure_selects_keycloak_authenticator(): + c, mod = _configure_with_defaults() + assert c.JupyterHub.authenticator_class is mod.KeyCloakOAuthenticator + + +def test_configure_derives_keycloak_urls_from_issuer(): + c, _ = _configure_with_defaults() + kc = c.KeyCloakOAuthenticator + assert kc.authorize_url == f"{ISSUER}/protocol/openid-connect/auth" + assert kc.token_url == f"{ISSUER}/protocol/openid-connect/token" + assert kc.userdata_url == f"{ISSUER}/protocol/openid-connect/userinfo" + + +def test_configure_sets_username_claim_to_preferred_username(): + c, _ = _configure_with_defaults() + assert c.KeyCloakOAuthenticator.username_claim == "preferred_username" + + +# --------------------------------------------------------------------------- +# Cycle 2 — auth_state persistence + group/admin claims +# --------------------------------------------------------------------------- + +def test_configure_enables_auth_state_so_refresh_user_can_run(): + c, _ = _configure_with_defaults() + kc = c.KeyCloakOAuthenticator + assert kc.enable_auth_state is True + assert kc.refresh_pre_spawn is True + # auth_refresh_age must be smaller than KC's 5-min access-token TTL + # so refresh fires before the token actually expires. + assert 0 < kc.auth_refresh_age <= 300 + + +def test_configure_reads_groups_claim_for_authorization(): + c, _ = _configure_with_defaults() + assert c.KeyCloakOAuthenticator.claim_groups_key == "groups" + + +def test_admin_groups_default_to_admin_when_unset(): + c, _ = _configure_with_defaults() + assert c.KeyCloakOAuthenticator.admin_groups == {"admin"} + + +def test_admin_groups_can_be_overridden_per_deployment(): + c, _ = _configure_with_defaults(admin_groups=["site-admins", "platform"]) + assert c.KeyCloakOAuthenticator.admin_groups == {"site-admins", "platform"} + + +# --------------------------------------------------------------------------- +# Cycle 3 — logout terminates the Keycloak session +# --------------------------------------------------------------------------- + +def test_logout_redirects_through_keycloak_end_session(): + """Hub logout must hit KC's end-session endpoint, not just clear local cookies. + + Otherwise the next /hub/ hit silently re-uses the live KC session. + """ + c, _ = _configure_with_defaults() + url = c.KeyCloakOAuthenticator.logout_redirect_url + assert url.startswith(f"{ISSUER}/protocol/openid-connect/logout"), url + + +def test_logout_url_includes_post_logout_redirect_uri(): + """After KC sign-out, the browser must return to the hub external URL.""" + c, _ = _configure_with_defaults() + url = c.KeyCloakOAuthenticator.logout_redirect_url + assert "post_logout_redirect_uri=" in url + # URL-encoded form of the external URL must appear (https%3A%2F%2F...) + assert "https%3A%2F%2Fhub.example.test" in url + + +def test_logout_url_url_encodes_external_url_safely(): + """Special characters in external_url must be safely percent-encoded.""" + c, _ = _configure_with_defaults(external_url="https://hub.example.test/?x=1&y=2") + url = c.KeyCloakOAuthenticator.logout_redirect_url + # Raw `&` from the external URL would prematurely terminate the query param. + assert "x%3D1%26y%3D2" in url + + +# --------------------------------------------------------------------------- +# Cycle 4 — authorization policy (any KC-authenticated user is allowed) +# --------------------------------------------------------------------------- + +def test_any_keycloak_authenticated_user_is_allowed_by_default(): + """The gateway path admitted any KC user; this path must match. + + Restricting to specific users/groups is a separate decision; the + default keeps parity with EnvoyOIDCAuthenticator so existing users + don't suddenly lose access on flip-day. + """ + c, _ = _configure_with_defaults() + assert c.Authenticator.allow_all is True + + +# --------------------------------------------------------------------------- +# Cycle 5 — production wiring is opt-in via env var +# --------------------------------------------------------------------------- + +def test_module_loads_in_jupyterhub_context_without_oauth_env(monkeypatch): + """Even when `c` is in scope (real JupyterHub run), missing env must not crash. + + Plain `kind` deploys ship the chart without the operator Secret. Hub + must come up with the chart's default authenticator (dummy) instead + of crashing on a missing /etc/oauth/client-id. + """ + for key in ("OAUTH_CALLBACK_URL", "OAUTH_EXTERNAL_URL", "OAUTH_SECRET_DIR"): + monkeypatch.delenv(key, raising=False) + + c = FakeConfig() + mod = load_config_module("00-gateway-auth.py", inject_c=c) + + # Sanity: the module still exposes its public surface. + assert callable(mod.configure) + # And it didn't try to wire up the authenticator on an empty config. + assert "JupyterHub" not in c.__dict__, ( + "Authenticator was configured despite missing OAUTH env vars" + ) From 16407c2a5f7d1b20329980c9bf064a8a8ff67b5f Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Thu, 14 May 2026 22:17:21 +0100 Subject: [PATCH 34/60] feat(auth): switch hub to KeyCloakOAuthenticator (GenericOAuthenticator) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hub now does its own OAuth dance with Keycloak instead of reading cookies that Envoy Gateway sets at the OIDC filter. JupyterHub's built-in refresh_user uses the stored refresh_token to keep auth_state fresh — no browser hit, no gateway-injected Bearer, no per-caller plumbing. Fixes the stale-token bug: /services/japps/conda-environments/ returned [] ~5 min after login because Envoy v1.6 doesn't rotate AccessToken-* cookie contents on every request, jhub-apps paths bypass hub, and the env-listing callable read user.auth_state which was frozen at OAuth callback time. Reads issuer-url / client-id / client-secret from a Secret mounted at /etc/oauth/ (overridable via OAUTH_SECRET_DIR); OAUTH_CALLBACK_URL and OAUTH_EXTERNAL_URL come from the deployment's env. Production wiring is gated on OAUTH_CALLBACK_URL being set, so plain kind deploys keep the chart-default authenticator (dummy). Logout points at KC's end_session_endpoint with a percent-encoded post_logout_redirect_uri so the upstream session is terminated, not just hub's local cookie. Replaces the 222-line EnvoyOIDCAuthenticator with a 100-line module behind a single configure() entry point. 12 unit tests cover URL derivation, auth_state/refresh wiring, admin/groups claims, logout URL encoding, and the env-gate. --- config/jupyterhub/00-gateway-auth.py | 315 +++++++++------------------ 1 file changed, 100 insertions(+), 215 deletions(-) diff --git a/config/jupyterhub/00-gateway-auth.py b/config/jupyterhub/00-gateway-auth.py index 5252a30..525a725 100644 --- a/config/jupyterhub/00-gateway-auth.py +++ b/config/jupyterhub/00-gateway-auth.py @@ -1,222 +1,107 @@ -"""Authenticator for Envoy Gateway OIDC. +"""JupyterHub authenticator that does its own OAuth flow with Keycloak. -When Envoy Gateway handles OIDC authentication, it stores the ID token -in a cookie (IdToken-). This authenticator reads that cookie, -decodes the JWT, and extracts the username — so users are automatically -logged into JupyterHub after authenticating with Keycloak at the gateway. +This module replaces the earlier EnvoyOIDCAuthenticator path where Envoy +Gateway acted as the OAuth client. Envoy v1.6 does not rotate cookie +contents on every request, so `auth_state` went stale for paths that +bypassed hub (e.g. `/services/japps/*`). + +With this module, hub is the OAuth client. JupyterHub's built-in +refresh_user uses the stored refresh_token to keep auth_state fresh +without depending on browser cookies or gateway-injected headers. + +The chart mounts the operator-created KC client Secret at +``/etc/oauth/`` (overridable via ``OAUTH_SECRET_DIR``); ``OAUTH_CALLBACK_URL`` +and ``OAUTH_EXTERNAL_URL`` come from chart-rendered envs. """ # ruff: noqa: F821 - `c` is a magic global provided by JupyterHub -import base64 -import json - -from jupyterhub.auth import Authenticator -from jupyterhub.handlers.login import LogoutHandler -from z2jh import get_config - - -class EnvoyOIDCLogoutHandler(LogoutHandler): - """Redirect to Envoy Gateway's OIDC logout endpoint after JupyterHub logout. - - The base LogoutHandler renders a static "Successfully logged out" page when - auto_login is True (to avoid redirect-loop back to auto-login). We override - render_logout_page to redirect to Envoy's /logout path instead, which clears - the OIDC cookies and terminates the Keycloak session. - """ - - async def render_logout_page(self): - self.redirect("/logout") - - -class EnvoyOIDCAuthenticator(Authenticator): - """Authenticate users from Envoy Gateway's OIDC IdToken cookie.""" - - auto_login = True - - # Re-read cookies every 60s so auth_state stays fresh. - # Envoy Gateway refreshes the AccessToken cookie automatically; - # this ensures JupyterHub's stored auth_state keeps up. - auth_refresh_age = 60 - - def get_handlers(self, app): - return [("/logout", EnvoyOIDCLogoutHandler)] - - @staticmethod - def _extract_envoy_cookies(handler): - """Extract Envoy Gateway OIDC tokens from the request. - - Returns (id_token, access_token, refresh_token) — any may be None. - - access_token is preferred from the `Authorization: Bearer` header - because Envoy injects a freshly-refreshed token there per request when - SecurityPolicy.oidc.forwardAccessToken=true. The `AccessToken-*` cookie - content is only updated at OAuth login (Envoy v1.6 doesn't rotate it - on background refresh), so the header is the only always-current - source. Fall back to the cookie when the header is absent (legacy - deployments where forwardAccessToken is off). - """ - id_token = None - access_token = None - refresh_token = None - - # Prefer Authorization: Bearer from Envoy. - auth_hdr = handler.request.headers.get("Authorization", "") - if auth_hdr.lower().startswith("bearer "): - access_token = auth_hdr.split(None, 1)[1].strip() or None - - for name, value in handler.request.cookies.items(): - if name.startswith("IdToken"): - id_token = value.value - elif name.startswith("AccessToken") and access_token is None: - access_token = value.value - elif name.startswith("RefreshToken"): - refresh_token = value.value - return id_token, access_token, refresh_token - - async def authenticate(self, handler, data=None): - # Envoy Gateway stores tokens as cookies after OIDC authentication: - # - # IdToken (IdToken-): - # JWT with user identity claims (sub, email, groups). - # Used here to extract the username and groups. - # - # AccessToken (AccessToken-): - # Short-lived credential (~5 min). Can be exchanged at Keycloak - # for a token with a different audience (RFC 8693). - # Stored in auth_state for the spawner and nebi env listing. - # - # RefreshToken (RefreshToken-): - # Envoy manages this internally (refreshToken: true in SecurityPolicy). - # NOT forwarded to upstream — the cookie is typically absent here. - # Envoy uses it to refresh the AccessToken cookie transparently. - id_token, access_token, refresh_token = self._extract_envoy_cookies(handler) - - if not id_token: - self.log.warning("No IdToken cookie found") - return None - - try: - # Decode JWT payload without verification — Envoy already validated it - payload_b64 = id_token.split(".")[1] - payload_b64 += "=" * (4 - len(payload_b64) % 4) - claims = json.loads(base64.urlsafe_b64decode(payload_b64)) - except Exception: - self.log.exception("Failed to decode IdToken JWT") - return None - - username = claims.get("preferred_username") or claims.get("sub") - if not username: - self.log.warning("authenticate: no username claim in IdToken: %s", list(claims.keys())) - return None - - # Extract groups from the token (set by the "groups" scope / group mapper) - groups = claims.get("groups", []) - if not groups: - self.log.warning( - "authenticate: no groups claim in IdToken for %s " - "(check Keycloak client scope / group mapper configuration)", - username, - ) - # Keycloak returns groups as paths (e.g. "/admin"), strip leading slash - groups = [g.strip("/") for g in groups] - - # Determine admin from group membership - admin_groups = set(get_config("custom.admin-groups", ["admin"])) - is_admin = bool(admin_groups & set(groups)) - self.log.info( - "authenticate: user=%s groups=%s is_admin=%s", - username, groups, is_admin, - ) - - return { - "name": username, - "admin": is_admin, - "groups": groups, - "auth_state": { - k: v for k, v in { - "id_token": id_token, - "access_token": access_token, - "refresh_token": refresh_token, - "groups": groups, # stored so spawner can read at spawn time - }.items() if v is not None - }, - } - - async def refresh_user(self, user, handler=None): - """Re-read Envoy's OIDC cookies to keep auth_state fresh. - - Envoy Gateway automatically refreshes the AccessToken cookie using - its internally managed refresh token. By reading the fresh cookie - here, we update auth_state so downstream consumers (nebi token - exchange, spawner hooks) always have a valid access token. - - Called by JupyterHub every `auth_refresh_age` seconds (60s). - """ - if handler is None: - # No request context (e.g. internal API call) — can't read cookies - self.log.debug("refresh_user: no handler for %s (internal call), returning True", user.name) - return True - - id_token, access_token, refresh_token = self._extract_envoy_cookies(handler) - - if not id_token: - # This method is called on an already-authenticated user — - # JupyterHub has already validated the request (via OAuth token - # or API token) before calling refresh_user. We are NOT doing - # authentication here; we're just deciding whether to update - # auth_state with fresh Envoy cookies or keep the existing session. - # - # Envoy OIDC cookies (IdToken, AccessToken) are only present on - # browser requests. Requests from singleuser pods (e.g. activity - # pings via JUPYTERHUB_API_TOKEN) have a handler but no cookies. - # Returning False here would invalidate the entire session, - # causing 403s on all subsequent requests including the browser. - # - # Instead, skip the auth_state refresh and keep the session alive. - # Envoy Gateway handles token refresh independently at the gateway - # layer — the next browser request will carry fresh cookies and - # update auth_state then. - self.log.debug( - "refresh_user: no IdToken cookie for %s, skipping auth_state refresh", - user.name, - ) - return True - - # Re-parse groups from the refreshed IdToken so auth_state stays current - try: - payload_b64 = id_token.split(".")[1] - payload_b64 += "=" * (4 - len(payload_b64) % 4) - claims = json.loads(base64.urlsafe_b64decode(payload_b64)) - groups = [g.strip("/") for g in claims.get("groups", [])] - self.log.debug("refresh_user: refreshed groups for %s: %s", user.name, groups) - except Exception: - self.log.warning( - "refresh_user: failed to parse groups from IdToken for %s " - "— auth_state will have empty groups until next full login", - user.name, - exc_info=True, - ) - groups = [] - - auth_state = { - k: v for k, v in { - "id_token": id_token, - "access_token": access_token, - "refresh_token": refresh_token, - "groups": groups, - }.items() if v is not None - } - - self.log.debug( - "refresh_user: auth_state refreshed for %s (access_token=%s, groups=%s)", - user.name, bool(access_token), groups, - ) - return {"name": user.name, "auth_state": auth_state} +import os +from pathlib import Path +from urllib.parse import quote + +from oauthenticator.generic import GenericOAuthenticator + + +class KeyCloakOAuthenticator(GenericOAuthenticator): + """Marker subclass so traitlets config can target it explicitly.""" + + +def _kc_urls(issuer: str) -> dict: + """Derive Keycloak OIDC endpoint URLs from the realm issuer URL.""" + base = f"{issuer}/protocol/openid-connect" + return { + "authorize_url": f"{base}/auth", + "token_url": f"{base}/token", + "userdata_url": f"{base}/userinfo", + "end_session_url": f"{base}/logout", + } + + +def configure( + c, + *, + issuer: str, + client_id: str, + client_secret: str, + callback_url: str, + external_url: str, + admin_groups=None, +): + """Wire KeyCloakOAuthenticator onto JupyterHub's `c` config object.""" + urls = _kc_urls(issuer) + c.JupyterHub.authenticator_class = KeyCloakOAuthenticator + c.KeyCloakOAuthenticator.client_id = client_id + c.KeyCloakOAuthenticator.client_secret = client_secret + c.KeyCloakOAuthenticator.oauth_callback_url = callback_url + c.KeyCloakOAuthenticator.authorize_url = urls["authorize_url"] + c.KeyCloakOAuthenticator.token_url = urls["token_url"] + c.KeyCloakOAuthenticator.userdata_url = urls["userdata_url"] + c.KeyCloakOAuthenticator.username_claim = "preferred_username" + c.KeyCloakOAuthenticator.claim_groups_key = "groups" + c.KeyCloakOAuthenticator.admin_groups = set(admin_groups or ["admin"]) + # Persist tokens so refresh_user can use the stored refresh_token. + c.KeyCloakOAuthenticator.enable_auth_state = True + c.KeyCloakOAuthenticator.refresh_pre_spawn = True + # Refresh ~1 min before KC's 5-min access-token TTL expires. + c.KeyCloakOAuthenticator.auth_refresh_age = 240 + # Hub logout must terminate the upstream Keycloak session, otherwise + # the next /hub/ request silently re-uses it. Bounce through KC's + # end_session_endpoint with post_logout_redirect_uri pointing back here. + c.KeyCloakOAuthenticator.logout_redirect_url = ( + f"{urls['end_session_url']}" + f"?post_logout_redirect_uri={quote(external_url, safe='')}" + ) + # Any KC-authenticated user is admitted (matches prior EnvoyOIDC policy); + # tighten via admin_groups / allowed_groups per-deploy if needed. + c.Authenticator.allow_all = True -if get_config("custom.external-url", ""): - c.JupyterHub.authenticator_class = EnvoyOIDCAuthenticator - # All users who pass Keycloak auth at the gateway are allowed - c.Authenticator.allow_all = True - c.Authenticator.enable_auth_state = True +def _read_secret_file(secret_dir: Path, key: str) -> str: + """Read a single value out of the operator-mounted KC client Secret.""" + return (secret_dir / key).read_text().strip() + + +# When loaded by JupyterHub, `c` is a magic global. On host imports (tests), +# `c` is undefined and the production wiring is skipped. +# +# Production wiring is gated TWICE: +# 1. `c` must exist (real JupyterHub run, not a host import). +# 2. `OAUTH_CALLBACK_URL` must be set (deployer opted into KC OAuth). +# Without (2), the chart's default authenticator (dummy) stays in place, +# so plain `kind` deploys come up without needing the operator Secret. +try: + c # type: ignore[used-before-def] +except NameError: + pass +else: + if os.environ.get("OAUTH_CALLBACK_URL"): + _secret_dir = Path(os.environ.get("OAUTH_SECRET_DIR", "/etc/oauth")) + configure( + c, # noqa: F821 + issuer=_read_secret_file(_secret_dir, "issuer-url"), + client_id=_read_secret_file(_secret_dir, "client-id"), + client_secret=_read_secret_file(_secret_dir, "client-secret"), + callback_url=os.environ["OAUTH_CALLBACK_URL"], + external_url=os.environ["OAUTH_EXTERNAL_URL"], + ) From d93c5ae1a236c1dd3dc6cdb5bdd9a4fb2a4fed39 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Thu, 14 May 2026 22:17:30 +0100 Subject: [PATCH 35/60] chore(chart): flip NebariApp to enforceAtGateway=false, hub OAuth callback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With hub doing its own OAuth (see KeyCloakOAuthenticator), Envoy SecurityPolicy on the hub host adds nothing — its cookie rotation lag was the original cause of the env-list stale-token bug. - enforceAtGateway: true -> false (operator drops the SecurityPolicy) - forwardAccessToken: true -> false (no longer relevant; avoid dual-token paths) - redirectURI: /oauth2/callback -> /hub/oauth_callback (JupyterHub default) Operator's provisionClient: true is independent of enforceAtGateway, so the KC client + Secret stay provisioned. The redirectURI change drives the operator to update the client's allowed redirect URI. --- values.yaml | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/values.yaml b/values.yaml index 4f407a8..29fc08c 100644 --- a/values.yaml +++ b/values.yaml @@ -21,28 +21,26 @@ nebariapp: enabled: true provider: keycloak provisionClient: true - redirectURI: /oauth2/callback # Envoy Gateway's default OIDC callback + # Hub does its own OAuth dance (see config/jupyterhub/00-gateway-auth.py), + # so the callback URL is JupyterHub's standard path, not Envoy's. + redirectURI: /hub/oauth_callback scopes: - openid - profile - email - groups - # enforceAtGateway runs the OIDC filter at Envoy Gateway. Required for - # the operator to manage the SecurityPolicy at all. - enforceAtGateway: true - # forwardAccessToken makes Envoy inject the user's freshly-refreshed access - # token as `Authorization: Bearer` on every upstream request. This is the - # only always-current source of the access token: Envoy v1.6 refreshes its - # internal token transparently but does not rotate the AccessToken-* cookie - # content, so cookie-based reads go stale ~5 min after login and break - # downstream token-exchange (jhub-apps env selector, Nebi auto-auth). - # - # Enabled by default: the JupyterHub image bundled by this chart pins - # jhub-apps to the head of nebari-dev/jhub-apps#676, which makes - # `get_current_user` fall through to the jhub-apps cookie when the - # Authorization header is not an HS256 wrapper (the historic redirect-loop - # source). Set to false only if running with an unpatched jhub-apps. - forwardAccessToken: true + # enforceAtGateway runs Envoy's OIDC filter for the host. Disabled here: + # when JupyterHub is its own OAuth client (GenericOAuthenticator with + # refresh_token grants), Envoy-level OIDC adds nothing and its cookie + # rotation lag stales out auth_state for /services/japps/* paths. + # Operator drops the SecurityPolicy on this flip via + # deleteSecurityPolicyIfExists; the KC client + Secret stay provisioned + # because provisionClient: true is independent of enforcement. + enforceAtGateway: false + # No longer needed once hub owns the OAuth flow — keeping it false avoids + # confusing dual-token paths between Envoy-injected Bearer and hub's own + # stored access_token. + forwardAccessToken: false # landingPage controls whether and how this service appears on the Nebari # landing page (requires nebari-operator with LandingPageConfig support). landingPage: From 161ab2dfaa8cd98e32e7aa7e001a9b6ee740b2ad Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Thu, 14 May 2026 22:17:39 +0100 Subject: [PATCH 36/60] =?UTF-8?q?revert(deps):=20drop=20jhub-apps=20git=20?= =?UTF-8?q?pin=20+=20relax=20pyjwt=20=E2=80=94=20hub=20owns=20OAuth=20now?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The temporary nebari-dev/jhub-apps#676 git pin (bearer fall-through + Starlette 1.0 TemplateResponse fix) plus pyjwt>=2.10 floor were only needed while Envoy was the OAuth client and could inject an RS256 Bearer that confused jhub-apps. With hub doing its own OAuth, Envoy no longer injects to /services/japps/* and neither workaround is needed. - jhub-apps: git@5d86277 -> ==2025.11.1 (conda-forge release) - pyjwt: >=2.10,<3 -> >=2.9,<2.10 (matches jhub-apps 2025.11.1 constraint) Lock regenerated via pixi 0.68.1 in a linux/arm64 container. --- images/jupyterhub/pixi.lock | 3109 ++++++++++++++++++----------------- images/jupyterhub/pixi.toml | 15 +- 2 files changed, 1613 insertions(+), 1511 deletions(-) diff --git a/images/jupyterhub/pixi.lock b/images/jupyterhub/pixi.lock index 656d490..fae22a6 100644 --- a/images/jupyterhub/pixi.lock +++ b/images/jupyterhub/pixi.lock @@ -9,144 +9,150 @@ environments: pypi-prerelease-mode: if-necessary-or-explicit packages: linux-64: - - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda - conda: https://conda.anaconda.org/conda-forge/noarch/aiohappyeyeballs-2.6.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/aiohttp-3.11.14-py310h89163eb_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/aiosignal-1.3.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/alembic-1.15.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aiohttp-3.13.5-py310h31b6992_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiosignal-1.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/alembic-1.18.4-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/annotated-types-0.7.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.3.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/async-timeout-5.0.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.4.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/async-timeout-5.0.1-pyhcf101f3_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/async_generator-1.10-pyhd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-25.3.0-pyh71513ae_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/backports.zstd-1.5.0-py310h69bd2ac_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/blinker-1.9.0-pyhff2d567_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py310hf71b8c6_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py310hba01987_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.6-hb03c661_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/ca-certificates-2025.1.31-hbcca054_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-hbd8a1cb_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/cachetools-5.5.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.1.31-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/certipy-0.2.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-1.17.1-py310h8deb56e_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/configurable-http-proxy-4.6.3-hbf95b10_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/cryptography-44.0.2-py310h6c63255_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/curl-8.18.0-h4e3cde8_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/durationpy-0.9-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.4.22-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/certipy-0.2.3-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.0.0-py310he7384ee_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/configurable-http-proxy-5.2.0-h5ac6406_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cryptography-46.0.7-py310hb288b08_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/curl-8.20.0-hcf29cc6_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/durationpy-0.10-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/escapism-1.0.1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/frozenlist-1.5.0-py310h89163eb_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/git-2.52.0-pl5321h6d3cee1_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/google-auth-2.38.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/greenlet-3.1.1-py310hf71b8c6_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.6.1-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.5.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/frozenlist-1.7.0-py310h9548a50_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/git-2.54.0-pl5321h6d3cee1_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/google-auth-2.52.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/greenlet-3.5.0-py310h25320af_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-78.3-h33c6efd_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.8.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/jsonpointer-3.0.0-py310hff52083_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.23.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2024.10.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.23.0-hd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.0-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonpointer-3.1.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.26.0-hcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterhub-5.1.0-pyh31011fe_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterhub-base-5.1.0-pyh31011fe_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterhub-idle-culler-1.2.1-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterhub-kubespawner-6.2.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.1-h166bdaf_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.3-h659f571_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.22.2-ha1258a1_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/kubernetes_asyncio-29.0.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.43-h712a8e2_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.18.0-h4e3cde8_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45.1-default_hbd61a6d_102.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlicommon-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.20.0-hcf29cc6_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.3-hecca717_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-14.2.0-h767d61c_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-14.2.0-h69a702a_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-14.2.0-h767d61c_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h4ce23a2_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.8.0-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h3b78370_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libidn2-2.3.8-hfac485b_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.6.4-hb9d3cd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.67.0-had1ee68_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hd590300_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.49.1-hee588c1_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.1-hf672d98_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-14.2.0-h8f9b012_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-14.2.0-h4852527_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.3-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.68.1-h877daf1_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.53.1-h0c1763c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.1-hcf80075_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_19.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libunistring-0.9.10-h7f98852_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libuv-1.50.0-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42-h5347b49_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuv-1.51.0-hb03c661_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mako-1.3.9-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.2-py310h89163eb_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/multidict-6.2.0-py310h89163eb_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/nodejs-22.13.0-hf235a45_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mako-1.3.12-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py310h3406613_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/multidict-6.7.1-py310h3406613_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.6-hdb14827_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/nodejs-24.14.1-h3d65ac4_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/oauthenticator-16.3.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/oauthlib-3.2.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.1-h35e630c_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/oauthlib-3.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.2-h35e630c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pamela-1.2.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.47-haa7fec5_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/perl-5.32.1-7_hd590300_perl5.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pkgutil-resolve-name-1.3.10-pyhd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.21.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/propcache-0.2.1-py310h89163eb_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.0.0-py310ha75aee5_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pyasn1-0.6.1-pyhd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pyasn1-modules-0.4.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.25.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/propcache-0.3.1-py310h89163eb_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.2.2-py310h139afa4_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyasn1-0.6.3-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyasn1-modules-0.4.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/pycurl-7.45.6-py310h6811363_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.10.6-pyh3cfb1c2_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/pydantic-core-2.27.2-py310h505e2c1_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pyjwt-2.12.1-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pyopenssl-25.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pycurl-7.46.0-py310h0aba9cc_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.13.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pydantic-core-2.46.4-py310hd8f68c5_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyjwt-2.9.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyopenssl-26.0.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.10.16-he725a3c_1_cpython.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhff2d567_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-2.0.7-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-kubernetes-32.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.10.20-h3c07f61_0_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-3.2.1-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-kubernetes-35.0.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-slugify-8.0.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.10-5_cp310.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.10-8_cp310.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pyu2f-0.1.5-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.2-py310h89163eb_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.36.2-pyh29332c3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py310h3406613_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/requests-oauthlib-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-0.23.1-py310hc1293b2_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/rsa-4.9-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/ruamel.yaml-0.18.10-py310ha75aee5_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/ruamel.yaml.clib-0.2.8-py310ha75aee5_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-75.8.2-pyhff2d567_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3987-syntax-1.1.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-0.30.0-py310hd8f68c5_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rsa-4.9.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ruamel.yaml-0.19.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ruamel.yaml.clib-0.2.15-py310h139afa4_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-82.0.1-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/sqlalchemy-1.4.46-py310h1fa729e_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/text-unidecode-1.3-pyhd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.4.2-py310ha75aee5_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/types-python-dateutil-2.9.0.20241206-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.12.2-hd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.12.2-pyha770c72_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025a-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.5-py310h7c4b9e2_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-inspection-0.4.2-pyhcf101f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-1.26.19-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-24.11.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.8.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/wget-1.21.4-hda4d442_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h7f98852_2.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/yarl-1.18.3-py310h89163eb_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.21.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/zlib-1.3.1-hb9d3cd8_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.7.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-25.10.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/wget-1.25.0-h653f8fd_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/yarl-1.23.0-py310h3406613_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zlib-1.3.2-h25fd6f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda - pypi: https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3b/00/2344469e2084fb287c2e0b57b72910309874c3245463acd6cf5e3db69324/appdirs-1.4.4-py2.py3-none-any.whl @@ -156,7 +162,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/e5/e2/c2e3abf398f80732e58b03be77bde9022550d221dd8781bf586bd4d97cc1/async_lru-2.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ea/cf/7fc0d8c3b5bae488ddab183a203474d9066fe77b0b563b3a345fc141ac50/backports_zstd-1.4.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cd/3a/577b549de0cc09d95f11087ee63c739bba856cd3952697eec4c4bb91350a/bleach-6.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/47/0b/bdf449df87be3f07b23091ceafee8c3ef569cf6d2fb7edec6e3b12b3faa4/bokeh-3.9.0-py3-none-any.whl @@ -164,6 +169,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/45/7f/0e961cf3908bc4c1c3e027de2794f867c6c89fb4916fc7dba295a0e80a2d/boltons-25.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ef/79/c45f2d53efe6ada1110cf6f9fca095e4ff47a0454444aefdde6ac4789179/cachecontrol-0.14.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bf/0f/f897abe4ea0a8c408ae65c8c83bffab4936ad65d6032d4fb4cd35bbdc3ee/cachetools-7.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/1a/aff8bb287a4b1400f69e09a53bd65de96aa5cee5691925b38731c67fc695/click_default_group-1.2.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl @@ -176,8 +182,8 @@ environments: - pypi: https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b9/f9/c248185bf982f2efbc02f1de5783b0e4aae94975b07d293e2aae1901c4b1/dulwich-0.24.1-cp310-cp310-manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/cb/a3/460c57f094a4a165c84a1341c373b0a4f5ec6ac244b998d5021aade89b77/ecdsa-0.19.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/07/7d/8f7a2eb39f97009001b30a8cdc2223856e56440f7069d7186d06f94e71a5/dulwich-0.24.10-cp310-cp310-manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/51/79/119091c98e2bf49e24ed9f3ae69f816d715d2904aefa6a2baa039a2ba0b0/ecdsa-0.19.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f5/64/82570019b199a85b1d9559e75f448272fc6a79201d8692a1357f3f803837/ensureconda-1.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl @@ -202,7 +208,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9a/93/242e2eab5fe682ffcb8b0084bde703a41d51e17ee0f3a31ff0d9d813620a/jedi-0.20.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl - - pypi: git+https://github.com/nebari-dev/jhub-apps.git?rev=5d86277f926136e0ecf253d36b261dd456727855#5d86277f926136e0ecf253d36b261dd456727855 + - pypi: https://files.pythonhosted.org/packages/2a/c9/c55b27c8a4fa2a266284d9120063370a28b04ff08bcd3b170e24c6ff573b/jhub_apps-2025.11.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b8/42/cf027b4ac873b076189d935b135397675dac80cb29acb13e1ab86ad6c631/json5-0.14.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/38/64/285f20a31679bf547b75602702f7800e74dbabae36ef324f716c02804753/jupyter-1.1.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2d/0b/ceb7694d864abc0a047649aec263878acb9f792e1fec3e676f22dc9015e3/jupyter_client-8.8.0-py3-none-any.whl @@ -221,8 +227,8 @@ environments: - pypi: https://files.pythonhosted.org/packages/c8/22/9460e311f340cb62d26a38c419b1381b8593b0bb6b5d1f056938b086d362/lockfile-0.12.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/71/d6/48f5b9e44e2e760855d7b489b1317cd7620e82dcb73197961e5cc1391348/mdit_py_plugins-0.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2a/7f/a946aa4f8752b37102b41e64dca18a1976ac705c3a0d1dfe74d820a02552/mistune-3.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cb/98/6af411189d9413534c3eb691182bff1f5c6d44ed2f93f2edfe52a1bbceb8/more_itertools-11.0.2-py3-none-any.whl @@ -256,8 +262,8 @@ environments: - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/41/4a/755a692976425af4627ffba016dd0576a045c70066553a27cc4023d44d58/python-keycloak-0.26.1.tar.gz - pypi: https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bd/2d/e94b2f7bab6773c70efc70a61d66e312e1febccd9e0db6b9e0adf58cbad1/python_jose-3.3.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/99/78/4126abcbdbd3c559d43e0db7f7b9173fc6befe45d39a2856cc0b8ec2a5a6/python_multipart-0.0.27-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d9/c3/0bd11992072e6a1c513b16500a5d07f91a24017c5909b02c72c62d7ad024/python_jose-3.5.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f3/a2/43bbc5860b5034e2af4ef99a0e04d726ff329c43e192ef3abaa8d7ecfce5/python_multipart-0.0.28-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ec/dd/96da98f892250475bdf2328112d7468abdd4acc7b902b6af23f4ed958ea0/pytz-2026.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/13/5a/f8c0868199bbb231a02616286ce8a4ccb85f5387b9215510297dcfedd214/pyviz_comms-3.0.6-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/30/76/8f099f9d6482450428b17c4d6b241281af7ce6a9de8149ca8c1c649f6792/pyzmq-27.1.0-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl @@ -274,161 +280,165 @@ environments: - pypi: https://files.pythonhosted.org/packages/a8/45/a132b9074aa18e799b891b91ad72133c98d8042c70f6240e4c5f9dabee2f/structlog-25.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b5/11/87d6d29fb5d237229d67973a6c9e06e048f01cf4994dee194ab0ea841814/tomlkit-0.14.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6a/43/8bd850ee71a191bf072e31302c73a66be413fecdd98fdcd111ecbcce13ca/tomlkit-0.15.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/e3/d81b065a2d866a33a541ac63a2a4cc5737e03ce2379ac3191c98bb8867e3/trove_classifiers-2026.5.7.17-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/61/73/d21edf5b204d1467e06500080a50f79d49ef2b997c79123a536d4a17d97c/uc_micro_py-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/43/99/3ec6335ded5b88c2f7ed25c56ffd952546f7ed007ffb1e1539dc3b57015a/userpath-1.9.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4c/f8/a5d5bac297b1379719050788c6b852c6b3eefcb1e82d8465ed22c10cede7/uv-0.11.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/31/a3/5b1562db76a5a488274b2332a97199b32d0442aca0ed193697fd47786316/uvicorn-0.46.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ca/ff/ded57ac5ff40a09e6e198550bab075d780941e0b0f83cbeabd087c59383a/virtualenv-20.33.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/12/4d/163fe746b97bd1129627e8b1f943e17583ddc143eaab532d56a799a9ba5a/uv-0.11.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/15/41/ac2dfdbc1f60c7af4f994c7a335cfa7040c01642b605d65f611cecc2a1e4/uvicorn-0.47.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f6/7f/89d7e8a397752826dbbb81127dfb8feab8286f4f1f91a820f646f04368fb/virtualenv-20.39.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/41/52/e465037f5375f43533d1a80b6923955201596a99142ed524d77b571a1418/wcwidth-0.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a8/a9/d23012099dc88ec69a29c6407b41d89681cb674c2043cd5b467c7e299c08/xyzservices-2026.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fd/e5/6d36f92a197c3c17729a2125e29c169f460538a7d939a27eaaa6dcfcba8e/zstandard-0.25.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl linux-aarch64: - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/_openmp_mutex-4.5-2_gnu.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/_openmp_mutex-4.5-20_gnu.conda - conda: https://conda.anaconda.org/conda-forge/noarch/aiohappyeyeballs-2.6.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/aiohttp-3.11.14-py310heeae437_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/aiosignal-1.3.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/alembic-1.15.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/aiohttp-3.13.5-py310h37690e7_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiosignal-1.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/alembic-1.18.4-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/annotated-types-0.7.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.3.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/async-timeout-5.0.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.4.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/async-timeout-5.0.1-pyhcf101f3_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/async_generator-1.10-pyhd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-25.3.0-pyh71513ae_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/backports.zstd-1.5.0-py310haa68bd9_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/blinker-1.9.0-pyhff2d567_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/brotli-python-1.1.0-py310he30c3ed_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/bzip2-1.0.8-h68df207_7.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/brotli-python-1.2.0-py310hbe54bbb_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/bzip2-1.0.8-h4777abc_9.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/c-ares-1.34.6-he30d5cf_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ca-certificates-2025.1.31-hcefe29a_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-hbd8a1cb_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/cachetools-5.5.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.1.31-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/certipy-0.2.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cffi-1.17.1-py310h1451162_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/configurable-http-proxy-4.6.3-h0ee932a_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cryptography-44.0.2-py310h42c23b7_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/curl-8.18.0-h7bfdcfb_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/durationpy-0.9-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.4.22-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/certipy-0.2.3-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cffi-2.0.0-py310h0826a50_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/configurable-http-proxy-5.2.0-h7e68a87_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cryptography-46.0.7-py310h868a156_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/curl-8.20.0-hc57f145_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/durationpy-0.10-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/escapism-1.0.1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/frozenlist-1.5.0-py310heeae437_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/git-2.52.0-pl5321h5dcfaa0_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/google-auth-2.38.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/greenlet-3.1.1-py310he30c3ed_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/icu-75.1-hf9b3779_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.6.1-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.5.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/frozenlist-1.7.0-py310h6b021dd_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/git-2.54.0-pl5321h5dcfaa0_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/google-auth-2.52.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/greenlet-3.5.0-py310heccc163_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/icu-78.3-hcab7f73_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.8.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/jsonpointer-3.0.0-py310h4c7bcd0_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.23.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2024.10.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.23.0-hd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.0-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonpointer-3.1.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.26.0-hcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterhub-5.1.0-pyh31011fe_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterhub-base-5.1.0-pyh31011fe_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterhub-idle-culler-1.2.1-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterhub-kubespawner-6.2.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/keyutils-1.6.1-h4e544f5_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/krb5-1.21.3-h50a48e9_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/keyutils-1.6.3-h86ecc28_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/krb5-1.22.2-hfd895c2_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/kubernetes_asyncio-29.0.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ld_impl_linux-aarch64-2.43-h80caac9_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libcurl-8.18.0-h7bfdcfb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ld_impl_linux-aarch64-2.45.1-default_h1979696_102.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libbrotlicommon-1.2.0-he30d5cf_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libbrotlidec-1.2.0-he30d5cf_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libbrotlienc-1.2.0-he30d5cf_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libcurl-8.20.0-hc57f145_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libedit-3.1.20250104-pl5321h976ea20_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libev-4.33-h31becfc_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libexpat-2.7.3-hfae3067_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libffi-3.4.6-he21f813_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgcc-14.2.0-he277a41_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgcc-ng-14.2.0-he9431aa_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgomp-14.2.0-he277a41_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libiconv-1.18-hc99b53d_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libexpat-2.8.0-hfae3067_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libffi-3.5.2-h376a255_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgcc-15.2.0-h8acb6b2_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgcc-ng-15.2.0-he9431aa_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgomp-15.2.0-h8acb6b2_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libiconv-1.18-h90929bb_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libidn2-2.3.8-h99ff5a0_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/liblzma-5.6.4-h86ecc28_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libnghttp2-1.67.0-ha888d0e_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libnsl-2.0.1-h31becfc_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libsqlite-3.49.1-h5eb1b54_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libssh2-1.11.1-ha41c0db_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libstdcxx-14.2.0-h3f4de04_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libstdcxx-ng-14.2.0-hf1166c9_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/liblzma-5.8.3-he30d5cf_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libnghttp2-1.68.1-hd3077d7_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libnsl-2.0.1-h86ecc28_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libsqlite-3.53.1-h022381a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libssh2-1.11.1-h18c354c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libstdcxx-15.2.0-hef695bb_19.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libunistring-0.9.10-hf897c2e_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libuuid-2.38.1-hb4cce97_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libuv-1.50.0-h86ecc28_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libuuid-2.42-h1022ec0_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libuv-1.51.0-he30d5cf_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libxcrypt-4.4.36-h31becfc_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libzlib-1.3.1-h86ecc28_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mako-1.3.9-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/markupsafe-3.0.2-py310h66848f9_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/multidict-6.2.0-py310heeae437_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ncurses-6.5-ha32ae93_3.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/nodejs-22.13.0-h8374285_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libzlib-1.3.2-hdc9db2a_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mako-1.3.12-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/markupsafe-3.0.3-py310h3b5aacf_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/multidict-6.7.1-py310h2d8da20_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ncurses-6.6-hf8d1292_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/nodejs-24.14.1-he5f9b4c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/oauthenticator-16.3.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/oauthlib-3.2.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/openssl-3.6.1-h546c87b_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/oauthlib-3.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/openssl-3.6.2-h546c87b_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pamela-1.2.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pcre2-10.47-hf841c20_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/perl-5.32.1-7_h31becfc_perl5.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pkgutil-resolve-name-1.3.10-pyhd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.21.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/propcache-0.2.1-py310heeae437_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/psutil-7.0.0-py310ha766c32_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pyasn1-0.6.1-pyhd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pyasn1-modules-0.4.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.25.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/propcache-0.3.1-py310heeae437_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/psutil-7.2.2-py310hef25091_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyasn1-0.6.3-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyasn1-modules-0.4.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pycurl-7.45.6-py310h25e8102_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.10.6-pyh3cfb1c2_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pydantic-core-2.27.2-py310h04a307d_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pyjwt-2.12.1-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pyopenssl-25.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pycurl-7.46.0-py310h48fd690_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.13.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pydantic-core-2.46.4-py310h598bb90_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyjwt-2.9.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyopenssl-26.0.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/python-3.10.16-h57b00e1_1_cpython.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhff2d567_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-2.0.7-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-kubernetes-32.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/python-3.10.20-h28be5d3_0_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-3.2.1-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-kubernetes-35.0.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-slugify-8.0.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/python_abi-3.10-5_cp310.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.10-8_cp310.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pyu2f-0.1.5-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pyyaml-6.0.2-py310heeae437_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/readline-8.2-h8382b9d_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.36.2-pyh29332c3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pyyaml-6.0.3-py310h2d8da20_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/readline-8.3-hb682ff5_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/requests-oauthlib-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/rpds-py-0.23.1-py310h2839ab5_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/rsa-4.9-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ruamel.yaml-0.18.10-py310ha766c32_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ruamel.yaml.clib-0.2.8-py310ha766c32_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-75.8.2-pyhff2d567_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3987-syntax-1.1.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/rpds-py-0.30.0-py310haddc216_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rsa-4.9.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ruamel.yaml-0.19.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ruamel.yaml.clib-0.2.15-py310hef25091_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-82.0.1-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/sqlalchemy-1.4.46-py310h734f5e8_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/text-unidecode-1.3-pyhd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/tk-8.6.13-h194ca79_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/tornado-6.4.2-py310h78583b1_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/types-python-dateutil-2.9.0.20241206-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.12.2-hd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.12.2-pyha770c72_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025a-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/tk-8.6.13-noxft_h0dc03b3_103.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/tornado-6.5.5-py310ha7967c6_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-inspection-0.4.2-pyhcf101f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-1.26.19-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-24.11.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.8.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/wget-1.21.4-h3861a24_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/yaml-0.2.5-hf897c2e_2.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/yarl-1.18.3-py310heeae437_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.21.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/zlib-1.3.1-h86ecc28_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/zstd-1.5.7-hbcf94c1_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.7.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-25.10.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/wget-1.25.0-h4f42960_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/yaml-0.2.5-h80f16a2_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/yarl-1.23.0-py310h2d8da20_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/zlib-1.3.2-hdc9db2a_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/zstd-1.5.7-h85ac4a6_6.conda - pypi: https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3b/00/2344469e2084fb287c2e0b57b72910309874c3245463acd6cf5e3db69324/appdirs-1.4.4-py2.py3-none-any.whl @@ -438,7 +448,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/e5/e2/c2e3abf398f80732e58b03be77bde9022550d221dd8781bf586bd4d97cc1/async_lru-2.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/90/a7/a521ea420aded33ca9821b3ef98e5bbdbd78d5d41711684cfce1a5d127a8/backports_zstd-1.4.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cd/3a/577b549de0cc09d95f11087ee63c739bba856cd3952697eec4c4bb91350a/bleach-6.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/47/0b/bdf449df87be3f07b23091ceafee8c3ef569cf6d2fb7edec6e3b12b3faa4/bokeh-3.9.0-py3-none-any.whl @@ -446,6 +455,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/45/7f/0e961cf3908bc4c1c3e027de2794f867c6c89fb4916fc7dba295a0e80a2d/boltons-25.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ef/79/c45f2d53efe6ada1110cf6f9fca095e4ff47a0454444aefdde6ac4789179/cachecontrol-0.14.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bf/0f/f897abe4ea0a8c408ae65c8c83bffab4936ad65d6032d4fb4cd35bbdc3ee/cachetools-7.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/1a/aff8bb287a4b1400f69e09a53bd65de96aa5cee5691925b38731c67fc695/click_default_group-1.2.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl @@ -458,8 +468,8 @@ environments: - pypi: https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/5e/a6/c13168490a926f84447e9035fb5d10504d7c4d81a68162a900c59b1fc0fe/dulwich-0.24.1-cp310-cp310-manylinux_2_28_aarch64.whl - - pypi: https://files.pythonhosted.org/packages/cb/a3/460c57f094a4a165c84a1341c373b0a4f5ec6ac244b998d5021aade89b77/ecdsa-0.19.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9b/71/97f4f643d29ff4737af36560ba669018868a748b1c8e778fc82038e830b8/dulwich-0.24.10-cp310-cp310-manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/51/79/119091c98e2bf49e24ed9f3ae69f816d715d2904aefa6a2baa039a2ba0b0/ecdsa-0.19.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f5/64/82570019b199a85b1d9559e75f448272fc6a79201d8692a1357f3f803837/ensureconda-1.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl @@ -484,7 +494,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9a/93/242e2eab5fe682ffcb8b0084bde703a41d51e17ee0f3a31ff0d9d813620a/jedi-0.20.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl - - pypi: git+https://github.com/nebari-dev/jhub-apps.git?rev=5d86277f926136e0ecf253d36b261dd456727855#5d86277f926136e0ecf253d36b261dd456727855 + - pypi: https://files.pythonhosted.org/packages/2a/c9/c55b27c8a4fa2a266284d9120063370a28b04ff08bcd3b170e24c6ff573b/jhub_apps-2025.11.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b8/42/cf027b4ac873b076189d935b135397675dac80cb29acb13e1ab86ad6c631/json5-0.14.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/38/64/285f20a31679bf547b75602702f7800e74dbabae36ef324f716c02804753/jupyter-1.1.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2d/0b/ceb7694d864abc0a047649aec263878acb9f792e1fec3e676f22dc9015e3/jupyter_client-8.8.0-py3-none-any.whl @@ -503,8 +513,8 @@ environments: - pypi: https://files.pythonhosted.org/packages/c8/22/9460e311f340cb62d26a38c419b1381b8593b0bb6b5d1f056938b086d362/lockfile-0.12.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/71/d6/48f5b9e44e2e760855d7b489b1317cd7620e82dcb73197961e5cc1391348/mdit_py_plugins-0.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2a/7f/a946aa4f8752b37102b41e64dca18a1976ac705c3a0d1dfe74d820a02552/mistune-3.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cb/98/6af411189d9413534c3eb691182bff1f5c6d44ed2f93f2edfe52a1bbceb8/more_itertools-11.0.2-py3-none-any.whl @@ -538,8 +548,8 @@ environments: - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/41/4a/755a692976425af4627ffba016dd0576a045c70066553a27cc4023d44d58/python-keycloak-0.26.1.tar.gz - pypi: https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bd/2d/e94b2f7bab6773c70efc70a61d66e312e1febccd9e0db6b9e0adf58cbad1/python_jose-3.3.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/99/78/4126abcbdbd3c559d43e0db7f7b9173fc6befe45d39a2856cc0b8ec2a5a6/python_multipart-0.0.27-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d9/c3/0bd11992072e6a1c513b16500a5d07f91a24017c5909b02c72c62d7ad024/python_jose-3.5.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f3/a2/43bbc5860b5034e2af4ef99a0e04d726ff329c43e192ef3abaa8d7ecfce5/python_multipart-0.0.28-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ec/dd/96da98f892250475bdf2328112d7468abdd4acc7b902b6af23f4ed958ea0/pytz-2026.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/13/5a/f8c0868199bbb231a02616286ce8a4ccb85f5387b9215510297dcfedd214/pyviz_comms-3.0.6-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/73/78/7d713284dbe022f6440e391bd1f3c48d9185673878034cfb3939cdf333b2/pyzmq-27.1.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl @@ -556,58 +566,48 @@ environments: - pypi: https://files.pythonhosted.org/packages/a8/45/a132b9074aa18e799b891b91ad72133c98d8042c70f6240e4c5f9dabee2f/structlog-25.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b5/11/87d6d29fb5d237229d67973a6c9e06e048f01cf4994dee194ab0ea841814/tomlkit-0.14.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/6a/43/8bd850ee71a191bf072e31302c73a66be413fecdd98fdcd111ecbcce13ca/tomlkit-0.15.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7b/e3/d81b065a2d866a33a541ac63a2a4cc5737e03ce2379ac3191c98bb8867e3/trove_classifiers-2026.5.7.17-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/61/73/d21edf5b204d1467e06500080a50f79d49ef2b997c79123a536d4a17d97c/uc_micro_py-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/43/99/3ec6335ded5b88c2f7ed25c56ffd952546f7ed007ffb1e1539dc3b57015a/userpath-1.9.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ee/d5/f3be167a43192062f1409fd6b857a612665d331174293b4ffc73218872e1/uv-0.11.11-py3-none-manylinux_2_28_aarch64.whl - - pypi: https://files.pythonhosted.org/packages/31/a3/5b1562db76a5a488274b2332a97199b32d0442aca0ed193697fd47786316/uvicorn-0.46.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ca/ff/ded57ac5ff40a09e6e198550bab075d780941e0b0f83cbeabd087c59383a/virtualenv-20.33.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/19/fb/7a3673494a0cf70267559166398f9c50c4925ff20122f99a28d6c5a80d83/uv-0.11.14-py3-none-manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/15/41/ac2dfdbc1f60c7af4f994c7a335cfa7040c01642b605d65f611cecc2a1e4/uvicorn-0.47.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f6/7f/89d7e8a397752826dbbb81127dfb8feab8286f4f1f91a820f646f04368fb/virtualenv-20.39.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/41/52/e465037f5375f43533d1a80b6923955201596a99142ed524d77b571a1418/wcwidth-0.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a8/a9/d23012099dc88ec69a29c6407b41d89681cb674c2043cd5b467c7e299c08/xyzservices-2026.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/73/28/a44bdece01bca027b079f0e00be3b6bd89a4df180071da59a3dd7381665b/zstandard-0.25.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl packages: -- conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 - sha256: fe51de6107f9edc7aa4f786a70f4a883943bc9d39b3bb7307c04c41410990726 - md5: d7c89558ba9fa0495403155b64376d81 - license: None - purls: [] - size: 2562 - timestamp: 1578324546067 -- conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 - build_number: 16 - sha256: fbe2c5e56a653bebb982eda4876a9178aedfc2b545f25d0ce9c4c0b508253d22 - md5: 73aaf86a425cc6e73fcf236a5a46396d - depends: - - _libgcc_mutex 0.1 conda_forge +- conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda + build_number: 20 + sha256: 1dd3fffd892081df9726d7eb7e0dea6198962ba775bd88842135a4ddb4deb3c9 + md5: a9f577daf3de00bca7c3c76c0ecbd1de + depends: + - __glibc >=2.17,<3.0.a0 - libgomp >=7.5.0 constrains: - - openmp_impl 9999 + - openmp_impl <0.0a0 license: BSD-3-Clause license_family: BSD purls: [] - size: 23621 - timestamp: 1650670423406 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/_openmp_mutex-4.5-2_gnu.tar.bz2 - build_number: 16 - sha256: 3702bef2f0a4d38bd8288bbe54aace623602a1343c2cfbefd3fa188e015bebf0 - md5: 6168d71addc746e8f2b8d57dfd2edcea + size: 28948 + timestamp: 1770939786096 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/_openmp_mutex-4.5-20_gnu.conda + build_number: 20 + sha256: a2527b1d81792a0ccd2c05850960df119c2b6d8f5fdec97f2db7d25dc23b1068 + md5: 468fd3bb9e1f671d36c2cbc677e56f1d depends: - libgomp >=7.5.0 constrains: - - openmp_impl 9999 + - openmp_impl <0.0a0 license: BSD-3-Clause license_family: BSD purls: [] - size: 23712 - timestamp: 1650670790230 + size: 28926 + timestamp: 1770939656741 - conda: https://conda.anaconda.org/conda-forge/noarch/aiohappyeyeballs-2.6.1-pyhd8ed1ab_0.conda sha256: 7842ddc678e77868ba7b92a726b437575b23aaec293bca0d40826f1026d90e27 md5: 18fd895e0e775622906cdabfc3cf0fb4 @@ -619,17 +619,17 @@ packages: - pkg:pypi/aiohappyeyeballs?source=hash-mapping size: 19750 timestamp: 1741775303303 -- conda: https://conda.anaconda.org/conda-forge/linux-64/aiohttp-3.11.14-py310h89163eb_0.conda - sha256: 70a0f3e0a18a69b409f196e051f0da80f474780ec03a1fe50d3ea950248a46d1 - md5: 85e5403139983ca1682502de260a1813 +- conda: https://conda.anaconda.org/conda-forge/linux-64/aiohttp-3.13.5-py310h31b6992_0.conda + sha256: e3714036a64509492fdf8e8283dba14a014360dd5d48790394d90e38bacbc8ac + md5: 85faca4e038614583ec627589b3dd14f depends: - __glibc >=2.17,<3.0.a0 - - aiohappyeyeballs >=2.3.0 - - aiosignal >=1.1.2 + - aiohappyeyeballs >=2.5.0 + - aiosignal >=1.4.0 - async-timeout >=4.0,<6.0 - attrs >=17.3.0 - frozenlist >=1.1.1 - - libgcc >=13 + - libgcc >=14 - multidict >=4.5,<7.0 - propcache >=0.2.0 - python >=3.10,<3.11.0a0 @@ -639,18 +639,18 @@ packages: license_family: Apache purls: - pkg:pypi/aiohttp?source=hash-mapping - size: 806671 - timestamp: 1742269779560 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/aiohttp-3.11.14-py310heeae437_0.conda - sha256: 2a9ea7eaafde28926d0bc4a7a2c696cc58b48b8b4b22d5d800065d3b92e8000e - md5: 59884e1ab8cc03a7703d324db79dc224 - depends: - - aiohappyeyeballs >=2.3.0 - - aiosignal >=1.1.2 + size: 902468 + timestamp: 1774999694212 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/aiohttp-3.13.5-py310h37690e7_0.conda + sha256: ed2f64d6d8d8f81e2e97ca9a0fcdb423df4b889e8164661a0018b7d50256ccd8 + md5: 27f3eb2dbe190b1fab4dc4ded7a25f9f + depends: + - aiohappyeyeballs >=2.5.0 + - aiosignal >=1.4.0 - async-timeout >=4.0,<6.0 - attrs >=17.3.0 - frozenlist >=1.1.1 - - libgcc >=13 + - libgcc >=14 - multidict >=4.5,<7.0 - propcache >=0.2.0 - python >=3.10,<3.11.0a0 @@ -661,34 +661,37 @@ packages: license_family: Apache purls: - pkg:pypi/aiohttp?source=hash-mapping - size: 799205 - timestamp: 1742268799014 -- conda: https://conda.anaconda.org/conda-forge/noarch/aiosignal-1.3.2-pyhd8ed1ab_0.conda - sha256: 7de8ced1918bbdadecf8e1c1c68237fe5709c097bd9e0d254f4cad118f4345d0 - md5: 1a3981115a398535dbe3f6d5faae3d36 + size: 893951 + timestamp: 1774999713267 +- conda: https://conda.anaconda.org/conda-forge/noarch/aiosignal-1.4.0-pyhd8ed1ab_0.conda + sha256: 8dc149a6828d19bf104ea96382a9d04dae185d4a03cc6beb1bc7b84c428e3ca2 + md5: 421a865222cd0c9d83ff08bc78bf3a61 depends: - frozenlist >=1.1.0 - python >=3.9 + - typing_extensions >=4.2 license: Apache-2.0 license_family: APACHE purls: - pkg:pypi/aiosignal?source=hash-mapping - size: 13229 - timestamp: 1734342253061 -- conda: https://conda.anaconda.org/conda-forge/noarch/alembic-1.15.1-pyhd8ed1ab_0.conda - sha256: 307da0338b29b8ef5aede18daa09d0b80c47cd62ffd4618167871fa9114cf098 - md5: 3b03a8229e613009c7832cb8e4d79cd9 + size: 13688 + timestamp: 1751626573984 +- conda: https://conda.anaconda.org/conda-forge/noarch/alembic-1.18.4-pyhcf101f3_0.conda + sha256: 83fc576dbcd59427f55be9623e1b101a1607ed9b4dc8633d86ada30c6ec1cf1d + md5: c45fa7cf996b766cb63eadf3c3e6408a depends: + - python >=3.10 + - sqlalchemy >=1.4.23 - mako - - python >=3.9 - - sqlalchemy >=1.4.0 - typing_extensions >=4.12 + - tomli + - python license: MIT license_family: MIT purls: - pkg:pypi/alembic?source=hash-mapping - size: 158380 - timestamp: 1741181935 + size: 184763 + timestamp: 1770806831769 - pypi: https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl name: annotated-doc version: 0.0.4 @@ -743,19 +746,20 @@ packages: - cffi>=1.0.1 ; python_full_version < '3.14' - cffi>=2.0.0b1 ; python_full_version >= '3.14' requires_python: '>=3.9' -- conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.3.0-pyhd8ed1ab_1.conda - sha256: c4b0bdb3d5dee50b60db92f99da3e4c524d5240aafc0a5fcc15e45ae2d1a3cd1 - md5: 46b53236fdd990271b03c3978d4218a9 +- conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.4.0-pyhcf101f3_0.conda + sha256: 792da8131b1b53ff667bd6fc617ea9087b570305ccb9913deb36b8e12b3b5141 + md5: 85c4f19f377424eafc4ed7911b291642 depends: - - python >=3.9 + - python >=3.10 - python-dateutil >=2.7.0 - - types-python-dateutil >=2.8.10 + - python-tzdata + - python license: Apache-2.0 - license_family: Apache + license_family: APACHE purls: - pkg:pypi/arrow?source=hash-mapping - size: 99951 - timestamp: 1733584345583 + size: 113854 + timestamp: 1760831179410 - pypi: https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl name: asttokens version: 3.0.1 @@ -774,17 +778,18 @@ packages: requires_dist: - typing-extensions>=4.0.0 ; python_full_version < '3.11' requires_python: '>=3.10' -- conda: https://conda.anaconda.org/conda-forge/noarch/async-timeout-5.0.1-pyhd8ed1ab_1.conda - sha256: 33d12250c870e06c9a313c6663cfbf1c50380b73dfbbb6006688c3134b29b45a - md5: 5d842988b11a8c3ab57fb70840c83d24 +- conda: https://conda.anaconda.org/conda-forge/noarch/async-timeout-5.0.1-pyhcf101f3_2.conda + sha256: 6638b68ab2675d0bed1f73562a4e75a61863b903be1538282cddb56c8e8f75bd + md5: 0d0ef7e4a0996b2c4ac2175a12b3bf69 depends: - - python >=3.9 + - python >=3.10 + - python license: Apache-2.0 - license_family: Apache + license_family: APACHE purls: - pkg:pypi/async-timeout?source=hash-mapping - size: 11763 - timestamp: 1733235428203 + size: 13559 + timestamp: 1767290444597 - conda: https://conda.anaconda.org/conda-forge/noarch/async_generator-1.10-pyhd8ed1ab_2.conda sha256: 31d9aa56d16f2d20bd5cfb5f8092147fdd5e3854c0beffa121d26937f4b4b3d7 md5: 0c07617cd436b9cd5570dc34f3af642b @@ -796,17 +801,18 @@ packages: - pkg:pypi/async-generator?source=hash-mapping size: 26916 timestamp: 1734180487831 -- conda: https://conda.anaconda.org/conda-forge/noarch/attrs-25.3.0-pyh71513ae_0.conda - sha256: 99c53ffbcb5dc58084faf18587b215f9ac8ced36bbfb55fa807c00967e419019 - md5: a10d11958cadc13fdb43df75f8b1903f +- conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda + sha256: 1b6124230bb4e571b1b9401537ecff575b7b109cc3a21ee019f65e083b8399ab + md5: c6b0543676ecb1fb2d7643941fe375f2 depends: - - python >=3.9 + - python >=3.10 + - python license: MIT license_family: MIT purls: - - pkg:pypi/attrs?source=compressed-mapping - size: 57181 - timestamp: 1741918625732 + - pkg:pypi/attrs?source=hash-mapping + size: 64927 + timestamp: 1773935801332 - pypi: https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl name: babel version: 2.18.0 @@ -839,16 +845,33 @@ packages: - jaraco-test ; extra == 'testing' - pytest!=8.0.* ; extra == 'testing' requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/90/a7/a521ea420aded33ca9821b3ef98e5bbdbd78d5d41711684cfce1a5d127a8/backports_zstd-1.4.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl - name: backports-zstd - version: 1.4.0 - sha256: e5d269184a1c310a69b2c3a8dcb6c87de90fae4e9252852918a740dbca780648 - requires_python: '>=3.10,<3.14' -- pypi: https://files.pythonhosted.org/packages/ea/cf/7fc0d8c3b5bae488ddab183a203474d9066fe77b0b563b3a345fc141ac50/backports_zstd-1.4.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - name: backports-zstd - version: 1.4.0 - sha256: 8b948ffb0697d1cdeed49ac20595532cdbe4968468426418d399c66e2a4c9dd3 - requires_python: '>=3.10,<3.14' +- conda: https://conda.anaconda.org/conda-forge/linux-64/backports.zstd-1.5.0-py310h69bd2ac_0.conda + sha256: d4fe868e503056a45ceed694e0263d1300bacbb5f021d402d40f3e3130b15703 + md5: 61e26e9f4f132463d161f9311b2ee736 + depends: + - python + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python_abi 3.10.* *_cp310 + - zstd >=1.5.7,<1.6.0a0 + license: BSD-3-Clause AND MIT AND EPL-2.0 + purls: + - pkg:pypi/backports-zstd?source=compressed-mapping + size: 192862 + timestamp: 1778594060634 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/backports.zstd-1.5.0-py310haa68bd9_0.conda + sha256: 8b17a6cae055df9a00ec4957752ab846f12bb641b654f13ac20e26e45793f75d + md5: cece3a20229a0a574f39ad5b27bbc336 + depends: + - python + - libgcc >=14 + - python_abi 3.10.* *_cp310 + - zstd >=1.5.7,<1.6.0a0 + license: BSD-3-Clause AND MIT AND EPL-2.0 + purls: + - pkg:pypi/backports-zstd?source=hash-mapping + size: 194178 + timestamp: 1778594052456 - pypi: https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl name: beautifulsoup4 version: 4.14.3 @@ -908,40 +931,40 @@ packages: version: 25.0.0 sha256: dc9fb38bf28985715497d1b54d00b62ea866eca3938938ea9043e254a3a6ca62 requires_python: '>=3.7' -- conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py310hf71b8c6_2.conda - sha256: 14f1e89d3888d560a553f40ac5ba83e4435a107552fa5b2b2029a7472554c1ef - md5: bf502c169c71e3c6ac0d6175addfacc2 +- conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py310hba01987_1.conda + sha256: f036fe554d902549f86689a9650a0996901d5c9242b0a1e3fbfe6dbccd2ae011 + md5: 393fca4557fbd2c4d995dcb89f569048 depends: - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - libstdcxx >=13 + - libgcc >=14 + - libstdcxx >=14 - python >=3.10,<3.11.0a0 - python_abi 3.10.* *_cp310 constrains: - - libbrotlicommon 1.1.0 hb9d3cd8_2 + - libbrotlicommon 1.2.0 hb03c661_1 license: MIT license_family: MIT purls: - pkg:pypi/brotli?source=hash-mapping - size: 349668 - timestamp: 1725267875087 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/brotli-python-1.1.0-py310he30c3ed_2.conda - sha256: 6cf28750939d74066f3380e2347391bdbd23311b3e3d2f998c9fd5add8e8e523 - md5: 3d7dcdcc65649814f571bf0671b5c5c8 + size: 367099 + timestamp: 1764017439384 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/brotli-python-1.2.0-py310hbe54bbb_1.conda + sha256: febea9d5c157f0a78dc0ffde32fe37b1f19eae812784f6e5505d471bf60d6f43 + md5: 49a57b1956565528e382d8c9af4854c7 depends: - - libgcc >=13 - - libstdcxx >=13 + - libgcc >=14 + - libstdcxx >=14 - python >=3.10,<3.11.0a0 - python >=3.10,<3.11.0a0 *_cpython - python_abi 3.10.* *_cp310 constrains: - - libbrotlicommon 1.1.0 h86ecc28_2 + - libbrotlicommon 1.2.0 he30d5cf_1 license: MIT license_family: MIT purls: - pkg:pypi/brotli?source=hash-mapping - size: 356792 - timestamp: 1725267937299 + size: 372450 + timestamp: 1764017428950 - pypi: https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl name: build version: 1.5.0 @@ -957,27 +980,27 @@ packages: - virtualenv>=20.17 ; python_full_version >= '3.10' and python_full_version < '3.14' and extra == 'virtualenv' - virtualenv>=20.31 ; python_full_version >= '3.14' and extra == 'virtualenv' requires_python: '>=3.10' -- conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda - sha256: 5ced96500d945fb286c9c838e54fa759aa04a7129c59800f0846b4335cee770d - md5: 62ee74e96c5ebb0af99386de58cf9553 +- conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda + sha256: 0b75d45f0bba3e95dc693336fa51f40ea28c980131fec438afb7ce6118ed05f6 + md5: d2ffd7602c02f2b316fd921d39876885 depends: - __glibc >=2.17,<3.0.a0 - - libgcc-ng >=12 + - libgcc >=14 license: bzip2-1.0.6 license_family: BSD purls: [] - size: 252783 - timestamp: 1720974456583 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/bzip2-1.0.8-h68df207_7.conda - sha256: 2258b0b33e1cb3a9852d47557984abb6e7ea58e3d7f92706ec1f8e879290c4cb - md5: 56398c28220513b9ea13d7b450acfb20 + size: 260182 + timestamp: 1771350215188 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/bzip2-1.0.8-h4777abc_9.conda + sha256: b3495077889dde6bb370938e7db82be545c73e8589696ad0843a32221520ad4c + md5: 840d8fc0d7b3209be93080bc20e07f2d depends: - - libgcc-ng >=12 + - libgcc >=14 license: bzip2-1.0.6 license_family: BSD purls: [] - size: 189884 - timestamp: 1720974504976 + size: 192412 + timestamp: 1771350241232 - conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.6-hb03c661_0.conda sha256: cc9accf72fa028d31c2a038460787751127317dcfa991f8d1f1babf216bb454e md5: 920bb03579f15389b9e512095ad995b7 @@ -999,20 +1022,15 @@ packages: purls: [] size: 217215 timestamp: 1765214743735 -- conda: https://conda.anaconda.org/conda-forge/linux-64/ca-certificates-2025.1.31-hbcca054_0.conda - sha256: bf832198976d559ab44d6cdb315642655547e26d826e34da67cbee6624cda189 - md5: 19f3a56f68d2fd06c516076bff482c52 - license: ISC - purls: [] - size: 158144 - timestamp: 1738298224464 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ca-certificates-2025.1.31-hcefe29a_0.conda - sha256: 66c6408ee461593cfdb2d78d82e6ed74d04a04ccb51df3ef8a5f35148c9c6eec - md5: 462cb166cd2e26a396f856510a3aff67 +- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-hbd8a1cb_0.conda + sha256: c9dbcc8039a52023660d6d1bbf87594a93dd69c6ac5a2a44323af2c92976728d + md5: e18ad67cf881dcadee8b8d9e2f8e5f73 + depends: + - __unix license: ISC purls: [] - size: 158290 - timestamp: 1738299057652 + size: 131039 + timestamp: 1776865545798 - pypi: https://files.pythonhosted.org/packages/ef/79/c45f2d53efe6ada1110cf6f9fca095e4ff47a0454444aefdde6ac4789179/cachecontrol-0.14.4-py3-none-any.whl name: cachecontrol version: 0.14.4 @@ -1058,46 +1076,41 @@ packages: - pkg:pypi/cached-property?source=hash-mapping size: 11065 timestamp: 1615209567874 -- conda: https://conda.anaconda.org/conda-forge/noarch/cachetools-5.5.2-pyhd8ed1ab_0.conda - sha256: 1823dc939b2c2b5354b6add5921434f9b873209a99569b3a2f24dca6c596c0d6 - md5: bf9c1698e819fab31f67dbab4256f7ba - depends: - - python >=3.9 - license: MIT - license_family: MIT - purls: - - pkg:pypi/cachetools?source=compressed-mapping - size: 15220 - timestamp: 1740094145914 -- conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.1.31-pyhd8ed1ab_0.conda - sha256: 42a78446da06a2568cb13e69be3355169fbd0ea424b00fc80b7d840f5baaacf3 - md5: c207fa5ac7ea99b149344385a9c0880d +- pypi: https://files.pythonhosted.org/packages/bf/0f/f897abe4ea0a8c408ae65c8c83bffab4936ad65d6032d4fb4cd35bbdc3ee/cachetools-7.1.1-py3-none-any.whl + name: cachetools + version: 7.1.1 + sha256: 0335cd7a0952d2b22327441fb0628139e234c565559eeb91a8a4ac7551c5353d + requires_python: '>=3.10' +- conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.4.22-pyhd8ed1ab_0.conda + sha256: 989db6e5957c4b44fa600c68c681ec2f36a55e48f7c7f1c073d5e91caa8cd878 + md5: 929471569c93acefb30282a22060dcd5 depends: - - python >=3.9 + - python >=3.10 license: ISC purls: - - pkg:pypi/certifi?source=compressed-mapping - size: 162721 - timestamp: 1739515973129 -- conda: https://conda.anaconda.org/conda-forge/noarch/certipy-0.2.1-pyhd8ed1ab_1.conda - sha256: c2ccd6295beb8c6b45b2f52a8d1c5a05b34fffd02c180f089a9e9c02e8dea9da - md5: 40cc21890ee2e9eca7cbc795d233e118 + - pkg:pypi/certifi?source=hash-mapping + size: 135656 + timestamp: 1776866680878 +- conda: https://conda.anaconda.org/conda-forge/noarch/certipy-0.2.3-pyhcf101f3_0.conda + sha256: 0e895b9bf38a795a05feef6f6954d1de15f833e652ec0d59fecdc4960ed2be58 + md5: 9d7f656740a8c0200e614b69ad8fccf9 depends: + - python >=3.10 - cryptography - - python >=3.9 + - python license: BSD-3-Clause license_family: BSD purls: - pkg:pypi/certipy?source=hash-mapping - size: 22361 - timestamp: 1734509823953 -- conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-1.17.1-py310h8deb56e_0.conda - sha256: 1b389293670268ab80c3b8735bc61bc71366862953e000efbb82204d00e41b6c - md5: 1fc24a3196ad5ede2a68148be61894f4 + size: 24467 + timestamp: 1777500624940 +- conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.0.0-py310he7384ee_1.conda + sha256: bf76ead6d59b70f3e901476a73880ac92011be63b151972d135eec55bbbe6091 + md5: 803e2d778b8dcccdc014127ec5001681 depends: - __glibc >=2.17,<3.0.a0 - - libffi >=3.4,<4.0a0 - - libgcc >=13 + - libffi >=3.5.2,<3.6.0a0 + - libgcc >=14 - pycparser - python >=3.10,<3.11.0a0 - python_abi 3.10.* *_cp310 @@ -1105,14 +1118,14 @@ packages: license_family: MIT purls: - pkg:pypi/cffi?source=hash-mapping - size: 243532 - timestamp: 1725560630552 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cffi-1.17.1-py310h1451162_0.conda - sha256: ba5d5c2a9c0c46138575283b6bed9e1131b25dd11dc8784af920da0d8d833892 - md5: c845d656240655e00d62f28efccac070 + size: 244766 + timestamp: 1761203011221 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cffi-2.0.0-py310h0826a50_1.conda + sha256: 63458040026be843a189e319190a0622486017c92ef251d4dff7ec847f9a8418 + md5: 152a5ba791642d8a81fe02d134ab3839 depends: - - libffi >=3.4,<4.0a0 - - libgcc >=13 + - libffi >=3.5.2,<3.6.0a0 + - libgcc >=14 - pycparser - python >=3.10,<3.11.0a0 - python_abi 3.10.* *_cp310 @@ -1120,19 +1133,19 @@ packages: license_family: MIT purls: - pkg:pypi/cffi?source=hash-mapping - size: 260852 - timestamp: 1725562359947 -- conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.1-pyhd8ed1ab_0.conda - sha256: 4e0ee91b97e5de3e74567bdacea27f0139709fceca4db8adffbe24deffccb09b - md5: e83a31202d1c0a000fce3e9cf3825875 + size: 261471 + timestamp: 1761204343202 +- conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda + sha256: 3f9483d62ce24ecd063f8a5a714448445dc8d9e201147c46699fc0033e824457 + md5: a9167b9571f3baa9d448faa2139d1089 depends: - - python >=3.9 + - python >=3.10 license: MIT license_family: MIT purls: - pkg:pypi/charset-normalizer?source=hash-mapping - size: 47438 - timestamp: 1735929811779 + size: 58872 + timestamp: 1775127203018 - pypi: https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl name: click version: 8.3.3 @@ -1238,26 +1251,26 @@ packages: - sphinx-autodoc-typehints>=1.19.2 ; extra == 'docs' - sphinx>=5.1.1 ; extra == 'docs' requires_python: '>=3.8' -- conda: https://conda.anaconda.org/conda-forge/linux-64/configurable-http-proxy-4.6.3-hbf95b10_0.conda - sha256: f3f6b2ceeec8134289b530be884340ad978263c618d6fec84647c3ee482ebf80 - md5: 7ede2481c094a10d4958e255ccaf3268 +- conda: https://conda.anaconda.org/conda-forge/linux-64/configurable-http-proxy-5.2.0-h5ac6406_0.conda + sha256: bf1afe636d0430928f26468e4c41dbff8ada8534c34f289c4818a6c224bf5c57 + md5: dc16ef5e20f4c2467c5f84f749e51d9a depends: - - nodejs >=22.12.0,<23.0a0 + - nodejs >=24.14.0,<25.0a0 license: BSD-3-Clause license_family: BSD purls: [] - size: 1291792 - timestamp: 1736705694523 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/configurable-http-proxy-4.6.3-h0ee932a_0.conda - sha256: 1416b01747cca91317177a89f83a7e957ac7ac71ea122bc9489faff058d24444 - md5: c7e189f9c8ad361916ca968a47e4e952 + size: 1401406 + timestamp: 1773275784615 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/configurable-http-proxy-5.2.0-h7e68a87_0.conda + sha256: ece70461790c32b357b191da3ca3e21026affb4c2928c9a88de93467109927ef + md5: 490e9e53b1a00b7c55c02d9bf5ec8409 depends: - - nodejs >=22.12.0,<23.0a0 + - nodejs >=24.13.1,<25.0a0 license: BSD-3-Clause license_family: BSD purls: [] - size: 1292600 - timestamp: 1736705738534 + size: 1402313 + timestamp: 1773275799995 - pypi: https://files.pythonhosted.org/packages/32/5c/1ee32d1c7956923202f00cf8d2a14a62ed7517bdc0ee1e55301227fc273c/contourpy-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl name: contourpy version: 1.3.2 @@ -1313,75 +1326,77 @@ packages: version: 0.4.1 sha256: 8d23eac5fa660409f57472e3851dab7ac18aba459a8d19cbbba86d3d5aecd2a5 requires_python: '>=3.7,<4.0' -- conda: https://conda.anaconda.org/conda-forge/linux-64/cryptography-44.0.2-py310h6c63255_0.conda - sha256: 9794c829047f951e8f23d70e6521e2fcae2e43e0dd1589dab69c791f6995bfc7 - md5: 24e325f1ed329640c3e4fc7add288363 +- conda: https://conda.anaconda.org/conda-forge/linux-64/cryptography-46.0.7-py310hb288b08_0.conda + sha256: 4b08d75510ffa2e8b1c97c7e52e5c73920941bee1075b6e28c2d717412007c95 + md5: 4b75ede0409f12819a914bcf81203e62 depends: - __glibc >=2.17,<3.0.a0 - - cffi >=1.12 - - libgcc >=13 - - openssl >=3.4.1,<4.0a0 + - cffi >=1.14 + - libgcc >=14 + - openssl >=3.5.6,<4.0a0 - python >=3.10,<3.11.0a0 - python_abi 3.10.* *_cp310 + - typing_extensions >=4.13.2 constrains: - __glibc >=2.17 license: Apache-2.0 AND BSD-3-Clause AND PSF-2.0 AND MIT license_family: BSD purls: - pkg:pypi/cryptography?source=hash-mapping - size: 1550536 - timestamp: 1740893772930 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cryptography-44.0.2-py310h42c23b7_0.conda - sha256: bf59887eb27d6fd62acb788c65fc798c3354caee78a9308efc52f5d629f96d52 - md5: 7b631eabba080b6a7c6847e763f0deaa + size: 2397277 + timestamp: 1775637827435 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cryptography-46.0.7-py310h868a156_0.conda + sha256: fb2499e6ca5339d2dc1300d7c9c199067fa74fe9313ebbc77229a112618d23fd + md5: 957ff4d99e44bceb72250f66a4a80b68 depends: - - cffi >=1.12 - - libgcc >=13 - - openssl >=3.4.1,<4.0a0 + - cffi >=1.14 + - libgcc >=14 + - openssl >=3.5.6,<4.0a0 - python >=3.10,<3.11.0a0 - python >=3.10,<3.11.0a0 *_cpython - python_abi 3.10.* *_cp310 + - typing_extensions >=4.13.2 constrains: - __glibc >=2.17 license: Apache-2.0 AND BSD-3-Clause AND PSF-2.0 AND MIT license_family: BSD purls: - pkg:pypi/cryptography?source=hash-mapping - size: 1528471 - timestamp: 1740893981886 -- conda: https://conda.anaconda.org/conda-forge/linux-64/curl-8.18.0-h4e3cde8_0.conda - sha256: f6f74fcb3a5a5239d8b876e9193df04dfcb1c5866e172797da657fdee9282b84 - md5: 261410cab40c7142adce3a09e24cae41 + size: 2368136 + timestamp: 1775637631591 +- conda: https://conda.anaconda.org/conda-forge/linux-64/curl-8.20.0-hcf29cc6_0.conda + sha256: 24b6ccc111388df77c65c68b3f3cad9f066e11741469fa60052ad0773f941c6e + md5: cc1a446bff91be88b2fa1d629e4f348b depends: - __glibc >=2.17,<3.0.a0 - - krb5 >=1.21.3,<1.22.0a0 - - libcurl 8.18.0 h4e3cde8_0 + - krb5 >=1.22.2,<1.23.0a0 + - libcurl 8.20.0 hcf29cc6_0 - libgcc >=14 - libssh2 >=1.11.1,<2.0a0 - - libzlib >=1.3.1,<2.0a0 - - openssl >=3.5.4,<4.0a0 + - libzlib >=1.3.2,<2.0a0 + - openssl >=3.5.6,<4.0a0 - zstd >=1.5.7,<1.6.0a0 license: curl license_family: MIT purls: [] - size: 190096 - timestamp: 1767821756587 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/curl-8.18.0-h7bfdcfb_0.conda - sha256: 69902ab9fd6b3e3f93c295bfc66c619a402a87788a95a3c9aca22d9ac98f6d7e - md5: aa7fb193d56058fefb901a263a48c88d - depends: - - krb5 >=1.21.3,<1.22.0a0 - - libcurl 8.18.0 h7bfdcfb_0 + size: 191489 + timestamp: 1777461498522 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/curl-8.20.0-hc57f145_0.conda + sha256: 7812bfa95c64f6900deff0d26091599a969b216b9bec1c3156f974cf060e5ca7 + md5: bf7d70846cc20c1bfdc229784c615103 + depends: + - krb5 >=1.22.2,<1.23.0a0 + - libcurl 8.20.0 hc57f145_0 - libgcc >=14 - libssh2 >=1.11.1,<2.0a0 - - libzlib >=1.3.1,<2.0a0 - - openssl >=3.5.4,<4.0a0 + - libzlib >=1.3.2,<2.0a0 + - openssl >=3.5.6,<4.0a0 - zstd >=1.5.7,<1.6.0a0 license: curl license_family: MIT purls: [] - size: 195461 - timestamp: 1767821684720 + size: 197100 + timestamp: 1777461491381 - pypi: https://files.pythonhosted.org/packages/e0/c3/7f67dea8ccf8fdcb9c99033bbe3e90b9e7395415843accb81428c441be2d/debugpy-1.8.20-py2.py3-none-any.whl name: debugpy version: 1.8.20 @@ -1401,57 +1416,61 @@ packages: name: distlib version: 0.4.0 sha256: 9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 -- pypi: https://files.pythonhosted.org/packages/5e/a6/c13168490a926f84447e9035fb5d10504d7c4d81a68162a900c59b1fc0fe/dulwich-0.24.1-cp310-cp310-manylinux_2_28_aarch64.whl +- pypi: https://files.pythonhosted.org/packages/07/7d/8f7a2eb39f97009001b30a8cdc2223856e56440f7069d7186d06f94e71a5/dulwich-0.24.10-cp310-cp310-manylinux_2_28_x86_64.whl name: dulwich - version: 0.24.1 - sha256: d16507ca6d6c2d29d7d942da4cc50fa589d58ab066030992dfa3932de6695062 + version: 0.24.10 + sha256: 90b24c0827299cfb53c4f4d4fedc811be5c4b10c11172ff6e5a5c52277fe0b3a requires_dist: - - urllib3>=1.25 - - typing-extensions>=4.0 ; python_full_version < '3.11' + - urllib3>=2.2.2 + - typing-extensions>=4.6.0 ; python_full_version < '3.12' - fastimport ; extra == 'fastimport' - - urllib3>=1.24.1 ; extra == 'https' + - urllib3>=2.2.2 ; extra == 'https' - gpg ; extra == 'pgp' - paramiko ; extra == 'paramiko' - rich ; extra == 'colordiff' - - ruff==0.12.4 ; extra == 'dev' - - mypy==1.17.0 ; extra == 'dev' + - ruff==0.13.2 ; extra == 'dev' + - mypy==1.18.2 ; extra == 'dev' - dissolve>=0.1.1 ; extra == 'dev' + - codespell==2.4.1 ; extra == 'dev' - merge3 ; extra == 'merge' - atheris ; extra == 'fuzzing' + - patiencediff ; extra == 'patiencediff' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/b9/f9/c248185bf982f2efbc02f1de5783b0e4aae94975b07d293e2aae1901c4b1/dulwich-0.24.1-cp310-cp310-manylinux_2_28_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/9b/71/97f4f643d29ff4737af36560ba669018868a748b1c8e778fc82038e830b8/dulwich-0.24.10-cp310-cp310-manylinux_2_28_aarch64.whl name: dulwich - version: 0.24.1 - sha256: e893b800c72499e21d0160169bac574292626193532c336ffce7617fe02d97db + version: 0.24.10 + sha256: 2a56f9838e5d2414a2b57bab370b73b9803fefd98836ef841f0fd489b5cc1349 requires_dist: - - urllib3>=1.25 - - typing-extensions>=4.0 ; python_full_version < '3.11' + - urllib3>=2.2.2 + - typing-extensions>=4.6.0 ; python_full_version < '3.12' - fastimport ; extra == 'fastimport' - - urllib3>=1.24.1 ; extra == 'https' + - urllib3>=2.2.2 ; extra == 'https' - gpg ; extra == 'pgp' - paramiko ; extra == 'paramiko' - rich ; extra == 'colordiff' - - ruff==0.12.4 ; extra == 'dev' - - mypy==1.17.0 ; extra == 'dev' + - ruff==0.13.2 ; extra == 'dev' + - mypy==1.18.2 ; extra == 'dev' - dissolve>=0.1.1 ; extra == 'dev' + - codespell==2.4.1 ; extra == 'dev' - merge3 ; extra == 'merge' - atheris ; extra == 'fuzzing' + - patiencediff ; extra == 'patiencediff' requires_python: '>=3.9' -- conda: https://conda.anaconda.org/conda-forge/noarch/durationpy-0.9-pyhd8ed1ab_1.conda - sha256: 045055a25f47473759eea9c710cc4b6d27a4f4db8e34d96771a830435d2e9a27 - md5: f7e6b04ecc949a19de49cbcd3f14addf +- conda: https://conda.anaconda.org/conda-forge/noarch/durationpy-0.10-pyhd8ed1ab_0.conda + sha256: 0aef1173052f05cb92beaed85d4dab0c3792ea08a4b9a22228068396e5c90078 + md5: 22a443792eb7e7d745fd1b04d4278c8e depends: - python >=3.9 license: MIT license_family: MIT purls: - pkg:pypi/durationpy?source=hash-mapping - size: 9454 - timestamp: 1734343666555 -- pypi: https://files.pythonhosted.org/packages/cb/a3/460c57f094a4a165c84a1341c373b0a4f5ec6ac244b998d5021aade89b77/ecdsa-0.19.1-py2.py3-none-any.whl + size: 10183 + timestamp: 1747576520071 +- pypi: https://files.pythonhosted.org/packages/51/79/119091c98e2bf49e24ed9f3ae69f816d715d2904aefa6a2baa039a2ba0b0/ecdsa-0.19.2-py2.py3-none-any.whl name: ecdsa - version: 0.19.1 - sha256: 30638e27cf77b7e15c4c4cc1973720149e1033827cfd00661ca5c8cc0cdb24c3 + version: 0.19.2 + sha256: 840f5dc5e375c68f36c1a7a5b9caad28f95daa65185c9253c0c08dd952bb7399 requires_dist: - six>=1.9.0 - gmpy ; extra == 'gmpy' @@ -1582,25 +1601,27 @@ packages: - pkg:pypi/fqdn?source=hash-mapping size: 16705 timestamp: 1733327494780 -- conda: https://conda.anaconda.org/conda-forge/linux-64/frozenlist-1.5.0-py310h89163eb_1.conda - sha256: 5a6142dddd42b3919ba2bd7ca9e2fd5af31516c180a7ec344d0f4795518fab6d - md5: 4e8901e0c05f60897ca052a4991c57e4 +- conda: https://conda.anaconda.org/conda-forge/linux-64/frozenlist-1.7.0-py310h9548a50_0.conda + sha256: c8abeb6da1e89113049d01c714fcce67e2fcc2853a63b3c40078372a5f66c59f + md5: 50e2b335c9da85d4eadaab11cf245415 depends: - __glibc >=2.17,<3.0.a0 - - libgcc >=13 + - libgcc >=14 + - libstdcxx >=14 - python >=3.10,<3.11.0a0 - python_abi 3.10.* *_cp310 license: Apache-2.0 license_family: APACHE purls: - pkg:pypi/frozenlist?source=hash-mapping - size: 60375 - timestamp: 1737645568121 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/frozenlist-1.5.0-py310heeae437_1.conda - sha256: d791241cb168f23a0d58438fcc70067565ca9c58c5806c40312fd0453edc9b8f - md5: 4a3a44ba47b72b05758c08c0647f3fb6 + size: 54180 + timestamp: 1752167428701 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/frozenlist-1.7.0-py310h6b021dd_0.conda + sha256: c0c7dafc7e95d9006c618266b3e4286401d75f1354c9fcfe27d347970d36516f + md5: 8d8625917118cd548b00ccaf073fd35e depends: - - libgcc >=13 + - libgcc >=14 + - libstdcxx >=14 - python >=3.10,<3.11.0a0 - python >=3.10,<3.11.0a0 *_cpython - python_abi 3.10.* *_cp310 @@ -1608,8 +1629,8 @@ packages: license_family: APACHE purls: - pkg:pypi/frozenlist?source=hash-mapping - size: 60622 - timestamp: 1737645501699 + size: 54459 + timestamp: 1752167321424 - pypi: https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl name: fsspec version: 2026.4.0 @@ -1718,40 +1739,40 @@ packages: - zstandard ; python_full_version < '3.14' and extra == 'test-full' - tqdm ; extra == 'tqdm' requires_python: '>=3.10' -- conda: https://conda.anaconda.org/conda-forge/linux-64/git-2.52.0-pl5321h6d3cee1_1.conda - sha256: 213eda4680ff80c59a146af0a664c4f3ee207c87e478ef323c7147dd5becacd3 - md5: 815606e45cf1c006ba346a6ca9e9eb9c +- conda: https://conda.anaconda.org/conda-forge/linux-64/git-2.54.0-pl5321h6d3cee1_0.conda + sha256: 718eb36fe23cac36c7bbeeb21ea0078256c8790b0e949a4ebb322e8e1da6c405 + md5: 25396e7aade67a4d4413431559a47591 depends: - __glibc >=2.28,<3.0.a0 - - libcurl >=8.17.0,<9.0a0 - - libexpat >=2.7.3,<3.0a0 + - libcurl >=8.20.0,<9.0a0 + - libexpat >=2.8.0,<3.0a0 - libgcc >=14 - libiconv >=1.18,<2.0a0 - - libzlib >=1.3.1,<2.0a0 - - openssl >=3.5.4,<4.0a0 + - libzlib >=1.3.2,<2.0a0 + - openssl >=3.5.6,<4.0a0 - pcre2 >=10.47,<10.48.0a0 - perl 5.* license: GPL-2.0-or-later and LGPL-2.1-or-later purls: [] - size: 11476073 - timestamp: 1763715359316 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/git-2.52.0-pl5321h5dcfaa0_1.conda - sha256: c56fff61cfa4b377622b79f6b1ffbbff8a55f6401fc5e576b8dbd15dbc25d562 - md5: e2e03a3acfc8de7c40c3447f2c9ced84 + size: 11369381 + timestamp: 1778072346246 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/git-2.54.0-pl5321h5dcfaa0_0.conda + sha256: dafadd010df9790501a6c7c1b7e1d3106d15aa5bf3d3bff50545fb637102bf78 + md5: 031e4cb27c961773de1d1646f48a3747 depends: - __glibc >=2.28,<3.0.a0 - - libcurl >=8.17.0,<9.0a0 - - libexpat >=2.7.3,<3.0a0 + - libcurl >=8.20.0,<9.0a0 + - libexpat >=2.8.0,<3.0a0 - libgcc >=14 - libiconv >=1.18,<2.0a0 - - libzlib >=1.3.1,<2.0a0 - - openssl >=3.5.4,<4.0a0 + - libzlib >=1.3.2,<2.0a0 + - openssl >=3.5.6,<4.0a0 - pcre2 >=10.47,<10.48.0a0 - perl 5.* license: GPL-2.0-or-later and LGPL-2.1-or-later purls: [] - size: 13827753 - timestamp: 1763720065676 + size: 15942075 + timestamp: 1778076259536 - pypi: https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl name: gitdb version: 4.0.12 @@ -1781,60 +1802,74 @@ packages: - sphinx-rtd-theme ; extra == 'doc' - sphinx-autodoc-typehints ; extra == 'doc' requires_python: '>=3.7' -- conda: https://conda.anaconda.org/conda-forge/noarch/google-auth-2.38.0-pyhd8ed1ab_0.conda - sha256: 0bbff264a2a50af0e2a61a4445c1b2353c6f44d87b83ffb36c95cca5d8fd4aaa - md5: c48abda87ffa7a0cc9f819cb8a384a9a +- conda: https://conda.anaconda.org/conda-forge/noarch/google-auth-2.52.0-pyhcf101f3_0.conda + sha256: 214e0efe01387501de54632de565b818fcbe150bd3feae81a00051eefd543b48 + md5: 2f2e9780fc900c0c5ee28a17dffc6ced depends: - - aiohttp >=3.6.2,<4.0.0 - - cachetools >=2.0.0,<6.0 - - cryptography >=38.0.3 + - python >=3.10 - pyasn1-modules >=0.2.1 + - cryptography >=38.0.3 + - aiohttp >=3.8.0,<4.0.0 + - requests >=2.20.0,<3.0.0 - pyopenssl >=20.0.0 - - python >=3.9 - pyu2f >=0.1.5 - - requests >=2.20.0,<3.0.0 - rsa >=3.1.4,<5 + - python license: Apache-2.0 - license_family: Apache + license_family: APACHE purls: - pkg:pypi/google-auth?source=hash-mapping - size: 116328 - timestamp: 1737618370547 -- conda: https://conda.anaconda.org/conda-forge/linux-64/greenlet-3.1.1-py310hf71b8c6_1.conda - sha256: 5a03a750d23a26a2660799f60a4cce4e951f5a5ee70db97216ae306b82401c61 - md5: 973d74c46d37ed8bbdbe721fb64a4357 + size: 146399 + timestamp: 1778229577801 +- conda: https://conda.anaconda.org/conda-forge/linux-64/greenlet-3.5.0-py310h25320af_0.conda + sha256: 1db4d16b9d2c39510be12979965abffc07ae4ccd68fe1621ac8401985647417d + md5: 21b186077b307024f3aa5b9070283da6 depends: + - python - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - libstdcxx >=13 - - python >=3.10,<3.11.0a0 + - libgcc >=14 + - libstdcxx >=14 - python_abi 3.10.* *_cp310 license: MIT license_family: MIT purls: - pkg:pypi/greenlet?source=hash-mapping - size: 214503 - timestamp: 1734532935475 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/greenlet-3.1.1-py310he30c3ed_1.conda - sha256: 2680051b5f37cba30088ea6449cc40d3c8ec177236ca649eda17fb9af212ff07 - md5: f0e06167a871874b814d4202f63dc870 + size: 238617 + timestamp: 1777328970047 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/greenlet-3.5.0-py310heccc163_0.conda + sha256: 03243c8023a1d31cd8c4f446bfb6cce45d1fbb689c56022c3d5501378620e32a + md5: 92b7d44805bfd2ce714c211186d810be depends: - - libgcc >=13 - - libstdcxx >=13 - - python >=3.10,<3.11.0a0 - - python >=3.10,<3.11.0a0 *_cpython + - python + - libstdcxx >=14 + - libgcc >=14 + - python 3.10.* *_cpython - python_abi 3.10.* *_cp310 license: MIT license_family: MIT purls: - pkg:pypi/greenlet?source=hash-mapping - size: 216950 - timestamp: 1734533013975 + size: 242363 + timestamp: 1777328971624 - pypi: https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl name: h11 version: 0.16.0 sha256: 63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86 requires_python: '>=3.8' +- conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + sha256: 84c64443368f84b600bfecc529a1194a3b14c3656ee2e832d15a20e0329b6da3 + md5: 164fc43f0b53b6e3a7bc7dce5e4f1dc9 + depends: + - python >=3.10 + - hyperframe >=6.1,<7 + - hpack >=4.1,<5 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/h2?source=hash-mapping + size: 95967 + timestamp: 1756364871835 - pypi: https://files.pythonhosted.org/packages/6b/29/742010d61a7665b863a36208bfa3df93476e9a86fde45413cd13db76f7d0/hatch-1.16.4-py3-none-any.whl name: hatch version: 1.16.4 @@ -1869,6 +1904,17 @@ packages: - tomli>=1.2.2 ; python_full_version < '3.11' - trove-classifiers requires_python: '>=3.10' +- conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + sha256: 6ad78a180576c706aabeb5b4c8ceb97c0cb25f1e112d76495bff23e3779948ba + md5: 0a802cb9888dd14eeefc611f05c40b6e + depends: + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/hpack?source=hash-mapping + size: 30731 + timestamp: 1737618390337 - pypi: https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl name: httpcore version: 1.0.9 @@ -1899,6 +1945,17 @@ packages: - socksio==1.* ; extra == 'socks' - zstandard>=0.18.0 ; extra == 'zstd' requires_python: '>=3.8' +- conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + sha256: 77af6f5fe8b62ca07d09ac60127a30d9069fdc3c68d6b256754d0ffb1f7779f8 + md5: 8e6923fc12f1fe8f8c4e5c9f343256ac + depends: + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/hyperframe?source=hash-mapping + size: 17397 + timestamp: 1737618427549 - pypi: https://files.pythonhosted.org/packages/6e/aa/8caf6a0a3e62863cbb9dab27135660acba46903b703e224f14f447e57934/hyperlink-21.0.0-py2.py3-none-any.whl name: hyperlink version: 21.0.0 @@ -1907,66 +1964,54 @@ packages: - idna>=2.5 - typing ; python_full_version < '3.5' requires_python: '>=2.6,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*' -- conda: https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda - sha256: 71e750d509f5fa3421087ba88ef9a7b9be11c53174af3aa4d06aff4c18b38e8e - md5: 8b189310083baabfb622af68fd9d3ae3 +- conda: https://conda.anaconda.org/conda-forge/linux-64/icu-78.3-h33c6efd_0.conda + sha256: fbf86c4a59c2ed05bbffb2ba25c7ed94f6185ec30ecb691615d42342baa1a16a + md5: c80d8a3b84358cb967fa81e7075fbc8a depends: - __glibc >=2.17,<3.0.a0 - - libgcc-ng >=12 - - libstdcxx-ng >=12 + - libgcc >=14 + - libstdcxx >=14 license: MIT license_family: MIT purls: [] - size: 12129203 - timestamp: 1720853576813 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/icu-75.1-hf9b3779_0.conda - sha256: 813298f2e54ef087dbfc9cc2e56e08ded41de65cff34c639cc8ba4e27e4540c9 - md5: 268203e8b983fddb6412b36f2024e75c + size: 12723451 + timestamp: 1773822285671 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/icu-78.3-hcab7f73_0.conda + sha256: 49ba6aed2c6b482bb0ba41078057555d29764299bc947b990708617712ef6406 + md5: 546da38c2fa9efacf203e2ad3f987c59 depends: - - libgcc-ng >=12 - - libstdcxx-ng >=12 + - libgcc >=14 + - libstdcxx >=14 license: MIT license_family: MIT purls: [] - size: 12282786 - timestamp: 1720853454991 -- conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda - sha256: d7a472c9fd479e2e8dcb83fb8d433fce971ea369d704ece380e876f9c3494e87 - md5: 39a4f67be3286c86d696df570b1201b7 + size: 12837286 + timestamp: 1773822650615 +- conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda + sha256: 9ab620e6f64bb67737bd7bc1ad6f480770124e304c6710617aba7fe60b089f48 + md5: fb7130c190f9b4ec91219840a05ba3ac depends: - - python >=3.9 + - python >=3.10 + - python license: BSD-3-Clause license_family: BSD purls: - pkg:pypi/idna?source=hash-mapping - size: 49765 - timestamp: 1733211921194 -- conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.6.1-pyha770c72_0.conda - sha256: 598951ebdb23e25e4cec4bbff0ae369cec65ead80b50bc08b441d8e54de5cf03 - md5: f4b39bf00c69f56ac01e020ebfac066c + size: 59038 + timestamp: 1776947141407 +- conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.8.0-pyhcf101f3_0.conda + sha256: 82ab2a0d91ca1e7e63ab6a4939356667ef683905dea631bc2121aa534d347b16 + md5: 080594bf4493e6bae2607e65390c520a depends: - - python >=3.9 - - zipp >=0.5 + - python >=3.10 + - zipp >=3.20 + - python license: Apache-2.0 license_family: APACHE purls: - pkg:pypi/importlib-metadata?source=hash-mapping - size: 29141 - timestamp: 1737420302391 -- conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.5.2-pyhd8ed1ab_0.conda - sha256: acc1d991837c0afb67c75b77fdc72b4bf022aac71fedd8b9ea45918ac9b08a80 - md5: c85c76dc67d75619a92f51dfbce06992 - depends: - - python >=3.9 - - zipp >=3.1.0 - constrains: - - importlib-resources >=6.5.2,<6.5.3.0a0 - license: Apache-2.0 - license_family: APACHE - purls: - - pkg:pypi/importlib-resources?source=hash-mapping - size: 33781 - timestamp: 1736252433366 + size: 34387 + timestamp: 1773931568510 - pypi: https://files.pythonhosted.org/packages/e5/ca/1172b6638d52f2d6caa2dd262ec4c811ba59eee96d54a7701930726bce18/installer-0.7.0-py3-none-any.whl name: installer version: 0.7.0 @@ -2210,9 +2255,10 @@ packages: - async-timeout ; python_full_version < '3.11' and extra == 'test' - trio ; extra == 'trio' requires_python: '>=3.7' -- pypi: git+https://github.com/nebari-dev/jhub-apps.git?rev=5d86277f926136e0ecf253d36b261dd456727855#5d86277f926136e0ecf253d36b261dd456727855 +- pypi: https://files.pythonhosted.org/packages/2a/c9/c55b27c8a4fa2a266284d9120063370a28b04ff08bcd3b170e24c6ff573b/jhub_apps-2025.11.1-py3-none-any.whl name: jhub-apps version: 2025.11.1 + sha256: 18f3b824234078ca5020d91147b03eb09771f1cc86a429f9844534fad28a1685 requires_dist: - bokeh - bokeh-root-cmd @@ -2226,7 +2272,7 @@ packages: - jupyterhub>4 - panel - plotlydash-tornado-cmd - - pyjwt>=2.10.0 + - pyjwt<2.10.0 - python-multipart - python-slugify - requests @@ -2243,96 +2289,85 @@ packages: - ruff ; extra == 'dev' - streamlit ; extra == 'dev' - voila ; extra == 'dev' - requires_python: '>=3.9' -- conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhd8ed1ab_0.conda - sha256: f1ac18b11637ddadc05642e8185a851c7fab5998c6f5470d716812fae943b2af - md5: 446bd6c8cb26050d528881df495ce646 + requires_python: '>=3.8' +- conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + sha256: fc9ca7348a4f25fed2079f2153ecdcf5f9cf2a0bc36c4172420ca09e1849df7b + md5: 04558c96691bed63104678757beb4f8d depends: - markupsafe >=2.0 - - python >=3.9 + - python >=3.10 + - python license: BSD-3-Clause license_family: BSD purls: - - pkg:pypi/jinja2?source=compressed-mapping - size: 112714 - timestamp: 1741263433881 + - pkg:pypi/jinja2?source=hash-mapping + size: 120685 + timestamp: 1764517220861 - pypi: https://files.pythonhosted.org/packages/b8/42/cf027b4ac873b076189d935b135397675dac80cb29acb13e1ab86ad6c631/json5-0.14.0-py3-none-any.whl name: json5 version: 0.14.0 sha256: 56cf861bab076b1178eb8c92e1311d273a9b9acea2ccc82c276abf839ebaef3a requires_python: '>=3.8.0' -- conda: https://conda.anaconda.org/conda-forge/linux-64/jsonpointer-3.0.0-py310hff52083_1.conda - sha256: ac8e92806a5017740b9a1113f0cab8559cd33884867ec7e99b556eb2fa847690 - md5: ce614a01b0aee1b29cee13d606bcb5d5 - depends: - - python >=3.10,<3.11.0a0 - - python_abi 3.10.* *_cp310 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/jsonpointer?source=hash-mapping - size: 15658 - timestamp: 1725302992487 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/jsonpointer-3.0.0-py310h4c7bcd0_1.conda - sha256: ae45ea4c3ea0eb6a553b5195bd357a7ba7e2fdfd2b426895ec7b1453c38c3629 - md5: 39313b5bbbd430af02824edd103ed1a7 +- conda: https://conda.anaconda.org/conda-forge/noarch/jsonpointer-3.1.1-pyhcf101f3_0.conda + sha256: a3d10301b6ff399ba1f3d39e443664804a3d28315a4fb81e745b6817845f70ae + md5: 89bf346df77603055d3c8fe5811691e6 depends: - - python >=3.10,<3.11.0a0 - - python >=3.10,<3.11.0a0 *_cpython - - python_abi 3.10.* *_cp310 + - python >=3.10 + - python license: BSD-3-Clause license_family: BSD purls: - pkg:pypi/jsonpointer?source=hash-mapping - size: 16196 - timestamp: 1725303127223 -- conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.23.0-pyhd8ed1ab_1.conda - sha256: be992a99e589146f229c58fe5083e0b60551d774511c494f91fe011931bd7893 - md5: a3cead9264b331b32fe8f0aabc967522 + size: 14190 + timestamp: 1774311356147 +- conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda + sha256: db973a37d75db8e19b5f44bbbdaead0c68dde745407f281e2a7fe4db74ec51d7 + md5: ada41c863af263cc4c5fcbaff7c3e4dc depends: - attrs >=22.2.0 - - importlib_resources >=1.4.0 - - jsonschema-specifications >=2023.03.6 - - pkgutil-resolve-name >=1.3.10 - - python >=3.9 + - jsonschema-specifications >=2023.3.6 + - python >=3.10 - referencing >=0.28.4 - - rpds-py >=0.7.1 + - rpds-py >=0.25.0 + - python license: MIT license_family: MIT purls: - pkg:pypi/jsonschema?source=hash-mapping - size: 74256 - timestamp: 1733472818764 -- conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2024.10.1-pyhd8ed1ab_1.conda - sha256: 37127133837444cf0e6d1a95ff5a505f8214ed4e89e8e9343284840e674c6891 - md5: 3b519bc21bc80e60b456f1e62962a766 + size: 82356 + timestamp: 1767839954256 +- conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda + sha256: 0a4f3b132f0faca10c89fdf3b60e15abb62ded6fa80aebfc007d05965192aa04 + md5: 439cd0f567d697b20a8f45cb70a1005a depends: - - python >=3.9 + - python >=3.10 - referencing >=0.31.0 + - python license: MIT license_family: MIT purls: - pkg:pypi/jsonschema-specifications?source=hash-mapping - size: 16170 - timestamp: 1733493624968 -- conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.23.0-hd8ed1ab_1.conda - sha256: 6e0184530011961a0802fda100ecdfd4b0eca634ed94c37e553b72e21c26627d - md5: a5b1a8065857cc4bd8b7a38d063bb728 + size: 19236 + timestamp: 1757335715225 +- conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.26.0-hcf101f3_0.conda + sha256: 6886fc61e4e4edd38fd38729976b134e8bd2143f7fce56cc80d7ac7bac99bce1 + md5: 8368d58342d0825f0843dc6acdd0c483 depends: + - jsonschema >=4.26.0,<4.26.1.0a0 - fqdn - idna - isoduration - jsonpointer >1.13 - - jsonschema >=4.23.0,<4.23.1.0a0 - rfc3339-validator - rfc3986-validator >0.1.0 + - rfc3987-syntax >=1.1.0 - uri-template - webcolors >=24.6.0 license: MIT license_family: MIT purls: [] - size: 7135 - timestamp: 1733472820035 + size: 4740 + timestamp: 1767839954258 - pypi: https://files.pythonhosted.org/packages/38/64/285f20a31679bf547b75602702f7800e74dbabae36ef324f716c02804753/jupyter-1.1.1-py2.py3-none-any.whl name: jupyter version: 1.1.1 @@ -2491,13 +2526,13 @@ packages: - pytest-timeout ; extra == 'test' - pytest>=7.0 ; extra == 'test' requires_python: '>=3.8' -- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.0-pyh29332c3_0.conda - sha256: 37e6ac3ccf7afcc730c3b93cb91a13b9ae827fd306f35dd28f958a74a14878b5 - md5: f56000b36f09ab7533877e695e4e8cb0 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.1-pyhcf101f3_0.conda + sha256: c7edb5682c6316a95ad781dccb1b6589cd2ec0bf94f23c21152974eb0363b5d7 + md5: bf42ee94c750c0b2e7e998b79ac299ea depends: - jsonschema-with-format-nongpl >=4.18.0 - packaging - - python >=3.9 + - python >=3.10 - python-json-logger >=2.0.4 - pyyaml >=5.3 - referencing @@ -2508,9 +2543,9 @@ packages: license: BSD-3-Clause license_family: BSD purls: - - pkg:pypi/jupyter-events?source=compressed-mapping - size: 23647 - timestamp: 1738765986736 + - pkg:pypi/jupyter-events?source=hash-mapping + size: 24002 + timestamp: 1776861872237 - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterhub-5.1.0-pyh31011fe_0.conda sha256: 61bd3d20c154d26902b824679b5c499a1047ff876d79fe87141d8aae296a24d8 md5: 076d727bb86cefca6653918a658d3836 @@ -2728,54 +2763,56 @@ packages: - types-pywin32 ; extra == 'type' - shtab>=1.1.0 ; extra == 'completion' requires_python: '>=3.9' -- conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.1-h166bdaf_0.tar.bz2 - sha256: 150c05a6e538610ca7c43beb3a40d65c90537497a4f6a5f4d15ec0451b6f5ebb - md5: 30186d27e2c9fa62b45fb1476b7200e3 +- conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda + sha256: 0960d06048a7185d3542d850986d807c6e37ca2e644342dd0c72feefcf26c2a4 + md5: b38117a3c920364aff79f870c984b4a3 depends: - - libgcc-ng >=10.3.0 + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 license: LGPL-2.1-or-later purls: [] - size: 117831 - timestamp: 1646151697040 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/keyutils-1.6.1-h4e544f5_0.tar.bz2 - sha256: 6d4233d97a9b38acbb26e1268bcf8c10a8e79c2aed7e5a385ec3769967e3e65b - md5: 1f24853e59c68892452ef94ddd8afd4b + size: 134088 + timestamp: 1754905959823 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/keyutils-1.6.3-h86ecc28_0.conda + sha256: 5ce830ca274b67de11a7075430a72020c1fb7d486161a82839be15c2b84e9988 + md5: e7df0aab10b9cbb73ab2a467ebfaf8c7 depends: - - libgcc-ng >=10.3.0 + - libgcc >=13 license: LGPL-2.1-or-later purls: [] - size: 112327 - timestamp: 1646166857935 -- conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.3-h659f571_0.conda - sha256: 99df692f7a8a5c27cd14b5fb1374ee55e756631b9c3d659ed3ee60830249b238 - md5: 3f43953b7d3fb3aaa1d0d0723d91e368 - depends: - - keyutils >=1.6.1,<2.0a0 - - libedit >=3.1.20191231,<3.2.0a0 - - libedit >=3.1.20191231,<4.0a0 - - libgcc-ng >=12 - - libstdcxx-ng >=12 - - openssl >=3.3.1,<4.0a0 + size: 129048 + timestamp: 1754906002667 +- conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.22.2-ha1258a1_0.conda + sha256: 3e307628ca3527448dd1cb14ad7bb9d04d1d28c7d4c5f97ba196ae984571dd25 + md5: fb53fb07ce46a575c5d004bbc96032c2 + depends: + - __glibc >=2.17,<3.0.a0 + - keyutils >=1.6.3,<2.0a0 + - libedit >=3.1.20250104,<3.2.0a0 + - libedit >=3.1.20250104,<4.0a0 + - libgcc >=14 + - libstdcxx >=14 + - openssl >=3.5.5,<4.0a0 license: MIT license_family: MIT purls: [] - size: 1370023 - timestamp: 1719463201255 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/krb5-1.21.3-h50a48e9_0.conda - sha256: 0ec272afcf7ea7fbf007e07a3b4678384b7da4047348107b2ae02630a570a815 - md5: 29c10432a2ca1472b53f299ffb2ffa37 - depends: - - keyutils >=1.6.1,<2.0a0 - - libedit >=3.1.20191231,<3.2.0a0 - - libedit >=3.1.20191231,<4.0a0 - - libgcc-ng >=12 - - libstdcxx-ng >=12 - - openssl >=3.3.1,<4.0a0 + size: 1386730 + timestamp: 1769769569681 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/krb5-1.22.2-hfd895c2_0.conda + sha256: b53999d888dda53c506b264e8c02b5f5c8e022c781eda0718f007339e6bc90ba + md5: d9ca108bd680ea86a963104b6b3e95ca + depends: + - keyutils >=1.6.3,<2.0a0 + - libedit >=3.1.20250104,<3.2.0a0 + - libedit >=3.1.20250104,<4.0a0 + - libgcc >=14 + - libstdcxx >=14 + - openssl >=3.5.5,<4.0a0 license: MIT license_family: MIT purls: [] - size: 1474620 - timestamp: 1719463205834 + size: 1517436 + timestamp: 1769773395215 - conda: https://conda.anaconda.org/conda-forge/noarch/kubernetes_asyncio-29.0.0-pyhd8ed1ab_0.conda sha256: 38a57b7c6929d50894f5587bc490224817e62c28e7718c731a51d3d0b86883cb md5: 9197a9d12cccf2dce9941395deaf5e70 @@ -2794,65 +2831,146 @@ packages: - pkg:pypi/kubernetes-asyncio?source=hash-mapping size: 483321 timestamp: 1705631401087 -- conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.43-h712a8e2_4.conda - sha256: db73f38155d901a610b2320525b9dd3b31e4949215c870685fd92ea61b5ce472 - md5: 01f8d123c96816249efd255a31ad7712 +- conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.1-pyhd8ed1ab_0.conda + sha256: 49570840fb15f5df5d4b4464db8ee43a6d643031a2bc70ef52120a52e3809699 + md5: 9b965c999135d43a3d0f7bd7d024e26a + depends: + - python >=3.10 + license: MIT + license_family: MIT + purls: + - pkg:pypi/lark?source=hash-mapping + size: 94312 + timestamp: 1761596921009 +- conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45.1-default_hbd61a6d_102.conda + sha256: 3d584956604909ff5df353767f3a2a2f60e07d070b328d109f30ac40cd62df6c + md5: 18335a698559cdbcd86150a48bf54ba6 depends: - __glibc >=2.17,<3.0.a0 + - zstd >=1.5.7,<1.6.0a0 constrains: - - binutils_impl_linux-64 2.43 + - binutils_impl_linux-64 2.45.1 license: GPL-3.0-only license_family: GPL purls: [] - size: 671240 - timestamp: 1740155456116 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ld_impl_linux-aarch64-2.43-h80caac9_4.conda - sha256: 016832a70b0aa97e1c4e47e23c00b0c34def679de25146736df353199f684f0d - md5: 80c9ad5e05e91bb6c0967af3880c9742 + size: 728002 + timestamp: 1774197446916 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ld_impl_linux-aarch64-2.45.1-default_h1979696_102.conda + sha256: 7abd913d81a9bf00abb699e8987966baa2065f5132e37e815f92d90fc6bba530 + md5: a21644fc4a83da26452a718dc9468d5f + depends: + - zstd >=1.5.7,<1.6.0a0 constrains: - - binutils_impl_linux-aarch64 2.43 + - binutils_impl_linux-aarch64 2.45.1 license: GPL-3.0-only license_family: GPL purls: [] - size: 699058 - timestamp: 1740155620594 + size: 875596 + timestamp: 1774197520746 - pypi: https://files.pythonhosted.org/packages/88/3f/ff00c588ebd7eae46a9d6223389f5ae28a3af4b6d975c0f2a6d86b1342b9/libarchive_c-5.3-py3-none-any.whl name: libarchive-c version: '5.3' sha256: 651550a6ec39266b78f81414140a1e04776c935e72dfc70f1d7c8e0a3672ffba -- conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.18.0-h4e3cde8_0.conda - sha256: 5454709d9fb6e9c3dd6423bc284fa7835a7823bfa8323f6e8786cdd555101fab - md5: 0a5563efed19ca4461cf927419b6eb73 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlicommon-1.2.0-hb03c661_1.conda + sha256: 318f36bd49ca8ad85e6478bd8506c88d82454cc008c1ac1c6bf00a3c42fa610e + md5: 72c8fd1af66bd67bf580645b426513ed + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: MIT + license_family: MIT + purls: [] + size: 79965 + timestamp: 1764017188531 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libbrotlicommon-1.2.0-he30d5cf_1.conda + sha256: 5fa8c163c8d776503aa68cdaf798ff9440c76a0a1c3ea84e0c43dbf1ece8af4d + md5: 8ec1d03f3000108899d1799d9964f281 + depends: + - libgcc >=14 + license: MIT + license_family: MIT + purls: [] + size: 80030 + timestamp: 1764017273715 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.2.0-hb03c661_1.conda + sha256: 12fff21d38f98bc446d82baa890e01fd82e3b750378fedc720ff93522ffb752b + md5: 366b40a69f0ad6072561c1d09301c886 + depends: + - __glibc >=2.17,<3.0.a0 + - libbrotlicommon 1.2.0 hb03c661_1 + - libgcc >=14 + license: MIT + license_family: MIT + purls: [] + size: 34632 + timestamp: 1764017199083 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libbrotlidec-1.2.0-he30d5cf_1.conda + sha256: 494365e8f58799ea95a6e82334ef696e9c2120aecd6626121694b30a15033301 + md5: 47e5b71b77bb8b47b4ecf9659492977f + depends: + - libbrotlicommon 1.2.0 he30d5cf_1 + - libgcc >=14 + license: MIT + license_family: MIT + purls: [] + size: 33166 + timestamp: 1764017282936 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.2.0-hb03c661_1.conda + sha256: a0c15c79997820bbd3fbc8ecf146f4fe0eca36cc60b62b63ac6cf78857f1dd0d + md5: 4ffbb341c8b616aa2494b6afb26a0c5f + depends: + - __glibc >=2.17,<3.0.a0 + - libbrotlicommon 1.2.0 hb03c661_1 + - libgcc >=14 + license: MIT + license_family: MIT + purls: [] + size: 298378 + timestamp: 1764017210931 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libbrotlienc-1.2.0-he30d5cf_1.conda + sha256: f998c03257b9aa1f7464446af2cf424862f0e54258a2a588309853e45ae771df + md5: 6553a5d017fe14859ea8a4e6ea5def8f + depends: + - libbrotlicommon 1.2.0 he30d5cf_1 + - libgcc >=14 + license: MIT + license_family: MIT + purls: [] + size: 309304 + timestamp: 1764017292044 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.20.0-hcf29cc6_0.conda + sha256: 75963a5dd913311f59a35dbd307592f4fa754c4808aff9c33edb430c415e38eb + md5: c3cc2864f82a944bc90a7beb4d3b0e88 depends: - __glibc >=2.17,<3.0.a0 - - krb5 >=1.21.3,<1.22.0a0 + - krb5 >=1.22.2,<1.23.0a0 - libgcc >=14 - - libnghttp2 >=1.67.0,<2.0a0 + - libnghttp2 >=1.68.1,<2.0a0 - libssh2 >=1.11.1,<2.0a0 - - libzlib >=1.3.1,<2.0a0 - - openssl >=3.5.4,<4.0a0 + - libzlib >=1.3.2,<2.0a0 + - openssl >=3.5.6,<4.0a0 - zstd >=1.5.7,<1.6.0a0 license: curl license_family: MIT purls: [] - size: 462942 - timestamp: 1767821743793 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libcurl-8.18.0-h7bfdcfb_0.conda - sha256: bf9d50e78df63b807c5cc98f44dc06a6555ab499edcd2949e9a07a5a785a11ee - md5: dc4f2007c6a30a45dfcf1c3a97b6aba6 + size: 468706 + timestamp: 1777461492876 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libcurl-8.20.0-hc57f145_0.conda + sha256: 1607d3caf58f7d9e9ad9e5f0841737d0cfa47b5501d4e36fbf98e1c645d7393e + md5: 88da514c8db1a7f6a05297941a897af2 depends: - - krb5 >=1.21.3,<1.22.0a0 + - krb5 >=1.22.2,<1.23.0a0 - libgcc >=14 - - libnghttp2 >=1.67.0,<2.0a0 + - libnghttp2 >=1.68.1,<2.0a0 - libssh2 >=1.11.1,<2.0a0 - - libzlib >=1.3.1,<2.0a0 - - openssl >=3.5.4,<4.0a0 + - libzlib >=1.3.2,<2.0a0 + - openssl >=3.5.6,<4.0a0 - zstd >=1.5.7,<1.6.0a0 license: curl license_family: MIT purls: [] - size: 482649 - timestamp: 1767821674919 + size: 488942 + timestamp: 1777461485901 - conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda sha256: d789471216e7aba3c184cd054ed61ce3f6dac6f87a50ec69291b9297f8c18724 md5: c277e0a4d549b03ac1e9d6cbbe3d017b @@ -2898,136 +3016,136 @@ packages: purls: [] size: 115123 timestamp: 1702146237623 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.3-hecca717_0.conda - sha256: 1e1b08f6211629cbc2efe7a5bca5953f8f6b3cae0eeb04ca4dacee1bd4e2db2f - md5: 8b09ae86839581147ef2e5c5e229d164 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.8.0-hecca717_0.conda + sha256: ea33c40977ea7a2c3658c522230058395bc2ee0d89d99f0711390b6a1ee80d12 + md5: a3b390520c563d78cc58974de95a03e5 depends: - __glibc >=2.17,<3.0.a0 - libgcc >=14 constrains: - - expat 2.7.3.* + - expat 2.8.0.* license: MIT license_family: MIT purls: [] - size: 76643 - timestamp: 1763549731408 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libexpat-2.7.3-hfae3067_0.conda - sha256: cc2581a78315418cc2e0bb2a273d37363203e79cefe78ba6d282fed546262239 - md5: b414e36fbb7ca122030276c75fa9c34a + size: 77241 + timestamp: 1777846112704 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libexpat-2.8.0-hfae3067_0.conda + sha256: 206c422a7f4b462d1dc17d558f0299088d0992bd3309ae83f5440fcc4f130602 + md5: 3bacd6171f0a3f8fddd06c3d5ae01955 depends: - libgcc >=14 constrains: - - expat 2.7.3.* + - expat 2.8.0.* license: MIT license_family: MIT purls: [] - size: 76201 - timestamp: 1763549910086 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_0.conda - sha256: 67a6c95e33ebc763c1adc3455b9a9ecde901850eb2fceb8e646cc05ef3a663da - md5: e3eb7806380bc8bcecba6d749ad5f026 + size: 76996 + timestamp: 1777846096032 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda + sha256: 31f19b6a88ce40ebc0d5a992c131f57d919f73c0b92cd1617a5bec83f6e961e6 + md5: a360c33a5abe61c07959e449fa1453eb depends: - __glibc >=2.17,<3.0.a0 - - libgcc >=13 + - libgcc >=14 license: MIT license_family: MIT purls: [] - size: 53415 - timestamp: 1739260413716 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libffi-3.4.6-he21f813_0.conda - sha256: 41568066beefe7b319ff27d85952242e5b77fb753d705b8716041959e17c35c2 - md5: 966084fccf3ad62a3160666cda869f28 + size: 58592 + timestamp: 1769456073053 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libffi-3.5.2-h376a255_0.conda + sha256: 3df4c539449aabc3443bbe8c492c01d401eea894603087fca2917aa4e1c2dea9 + md5: 2f364feefb6a7c00423e80dcb12db62a depends: - - libgcc >=13 + - libgcc >=14 license: MIT license_family: MIT purls: [] - size: 51513 - timestamp: 1739260449772 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-14.2.0-h767d61c_2.conda - sha256: 3a572d031cb86deb541d15c1875aaa097baefc0c580b54dc61f5edab99215792 - md5: ef504d1acbd74b7cc6849ef8af47dd03 + size: 55952 + timestamp: 1769456078358 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_19.conda + sha256: 8e0a3b5e41272e5678499b5dfc4cddb673f9e935de01eb0767ce857001229f46 + md5: 57736f29cc2b0ec0b6c2952d3f101b6a depends: - __glibc >=2.17,<3.0.a0 - _openmp_mutex >=4.5 constrains: - - libgomp 14.2.0 h767d61c_2 - - libgcc-ng ==14.2.0=*_2 + - libgcc-ng ==15.2.0=*_19 + - libgomp 15.2.0 he0feb66_19 license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL purls: [] - size: 847885 - timestamp: 1740240653082 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgcc-14.2.0-he277a41_2.conda - sha256: a57f7f9ba2a12f56eafdcd25b6d75f7be10b8fc1a802a58b76a77ca8c66f4503 - md5: 6b4268a60b10f29257b51b9b67ff8d76 + size: 1041084 + timestamp: 1778269013026 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgcc-15.2.0-h8acb6b2_19.conda + sha256: 4592b096e553f67799ae70d4b6167eeda3ec74587d68c7aecbf4e7b1df136681 + md5: f35b3f52d0a2ec4ffe3c89ba135cdb9a depends: - _openmp_mutex >=4.5 constrains: - - libgcc-ng ==14.2.0=*_2 - - libgomp 14.2.0 he277a41_2 + - libgomp 15.2.0 h8acb6b2_19 + - libgcc-ng ==15.2.0=*_19 license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL purls: [] - size: 535507 - timestamp: 1740241069780 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-14.2.0-h69a702a_2.conda - sha256: fb7558c328b38b2f9d2e412c48da7890e7721ba018d733ebdfea57280df01904 - md5: a2222a6ada71fb478682efe483ce0f92 + size: 622462 + timestamp: 1778268755949 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_19.conda + sha256: 9dcf54adfaa5e861123c2da4f2f0451a685464ea7e5a41ad91cf67b31d658d98 + md5: 331ee9b72b9dff570d56b1302c5ab37d depends: - - libgcc 14.2.0 h767d61c_2 + - libgcc 15.2.0 he0feb66_19 license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL purls: [] - size: 53758 - timestamp: 1740240660904 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgcc-ng-14.2.0-he9431aa_2.conda - sha256: 9647f75cddc18b07eebe6e1f21500eed50a6af2c43c84e831b4c7a597e10d226 - md5: 692c2bb75f32cfafb6799cf6d1c5d0e0 + size: 27694 + timestamp: 1778269016987 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgcc-ng-15.2.0-he9431aa_19.conda + sha256: 1137f93f477f56199ded24117430045a0c02cbe8b10031beac3b9ad2138539d3 + md5: 770cf892e5530f43e63cadc673e85653 depends: - - libgcc 14.2.0 he277a41_2 + - libgcc 15.2.0 h8acb6b2_19 license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL purls: [] - size: 53622 - timestamp: 1740241074834 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-14.2.0-h767d61c_2.conda - sha256: 1a3130e0b9267e781b89399580f3163632d59fe5b0142900d63052ab1a53490e - md5: 06d02030237f4d5b3d9a7e7d348fe3c6 + size: 27738 + timestamp: 1778268759211 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_19.conda + sha256: 5abe4ab9d93f6c9757d654f1969ae2267d4505315c1f2f8fe705fd60af084f1b + md5: faac990cb7aedc7f3a2224f2c9b0c26c depends: - __glibc >=2.17,<3.0.a0 license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL purls: [] - size: 459862 - timestamp: 1740240588123 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgomp-14.2.0-he277a41_2.conda - sha256: 4e303711fb7413bf98995beac58e731073099d7a669a3b81e49330ca8da05174 - md5: b11c09d9463daf4cae492d29806b1889 + size: 603817 + timestamp: 1778268942614 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgomp-15.2.0-h8acb6b2_19.conda + sha256: 2370ef0ffcbae5bede3c4bf136add4abc257245eb91f724c99bb4a43116c5a83 + md5: c5e8a379c4a2ec2aea4ba22758c001d9 license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL purls: [] - size: 462783 - timestamp: 1740241005079 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h4ce23a2_1.conda - sha256: 18a4afe14f731bfb9cf388659994263904d20111e42f841e9eea1bb6f91f4ab4 - md5: e796ff8ddc598affdf7c173d6145f087 + size: 587387 + timestamp: 1778268674393 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h3b78370_2.conda + sha256: c467851a7312765447155e071752d7bf9bf44d610a5687e32706f480aad2833f + md5: 915f5995e94f60e9a4826e0b0920ee88 depends: - __glibc >=2.17,<3.0.a0 - - libgcc >=13 + - libgcc >=14 license: LGPL-2.1-only purls: [] - size: 713084 - timestamp: 1740128065462 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libiconv-1.18-hc99b53d_1.conda - sha256: 3db14977036fe1f511a6dbecacbeff3fdb58482c5c0cf87a2ea3232f5a540836 - md5: 81541d85a45fbf4d0a29346176f1f21c + size: 790176 + timestamp: 1754908768807 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libiconv-1.18-h90929bb_2.conda + sha256: 1473451cd282b48d24515795a595801c9b65b567fe399d7e12d50b2d6cdb04d9 + md5: 5a86bf847b9b926f3a4f203339748d78 depends: - - libgcc >=13 + - libgcc >=14 license: LGPL-2.1-only purls: [] - size: 718600 - timestamp: 1740130562607 + size: 791226 + timestamp: 1754910975665 - conda: https://conda.anaconda.org/conda-forge/linux-64/libidn2-2.3.8-hfac485b_1.conda sha256: cc38c900b9a20fe75e61cbb594e749c57a06d96510722f5ddfa309682062b065 md5: 842a81de672ddcf476337c8bde3cad33 @@ -3051,165 +3169,154 @@ packages: purls: [] size: 147165 timestamp: 1760387531719 -- conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.6.4-hb9d3cd8_0.conda - sha256: cad52e10319ca4585bc37f0bc7cce99ec7c15dc9168e42ccb96b741b0a27db3f - md5: 42d5b6a0f30d3c10cd88cb8584fda1cb +- conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.3-hb03c661_0.conda + sha256: ec30e52a3c1bf7d0425380a189d209a52baa03f22fb66dd3eb587acaa765bd6d + md5: b88d90cad08e6bc8ad540cb310a761fb depends: - __glibc >=2.17,<3.0.a0 - - libgcc >=13 + - libgcc >=14 + constrains: + - xz 5.8.3.* license: 0BSD purls: [] - size: 111357 - timestamp: 1738525339684 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/liblzma-5.6.4-h86ecc28_0.conda - sha256: 96413664f0fade54a4931940d18749cfc8e6308349dbb0cb83adb2394ca1f730 - md5: b88244e0a115cc34f7fbca9b11248e76 + size: 113478 + timestamp: 1775825492909 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/liblzma-5.8.3-he30d5cf_0.conda + sha256: d61962b9cd54c3554361550203c64d5b65b71e3058a285b66e4b04b9769f0a5c + md5: 76298a9e6d71ee6e832a8d0d7373b261 depends: - - libgcc >=13 + - libgcc >=14 + constrains: + - xz 5.8.3.* license: 0BSD purls: [] - size: 124197 - timestamp: 1738528201520 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.67.0-had1ee68_0.conda - sha256: a4a7dab8db4dc81c736e9a9b42bdfd97b087816e029e221380511960ac46c690 - md5: b499ce4b026493a13774bcf0f4c33849 + size: 126102 + timestamp: 1775828008518 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.68.1-h877daf1_0.conda + sha256: 663444d77a42f2265f54fb8b48c5450bfff4388d9c0f8253dd7855f0d993153f + md5: 2a45e7f8af083626f009645a6481f12d depends: - __glibc >=2.17,<3.0.a0 - - c-ares >=1.34.5,<2.0a0 + - c-ares >=1.34.6,<2.0a0 - libev >=4.33,<4.34.0a0 - libev >=4.33,<5.0a0 - libgcc >=14 - libstdcxx >=14 - libzlib >=1.3.1,<2.0a0 - - openssl >=3.5.2,<4.0a0 + - openssl >=3.5.5,<4.0a0 license: MIT license_family: MIT purls: [] - size: 666600 - timestamp: 1756834976695 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libnghttp2-1.67.0-ha888d0e_0.conda - sha256: b03f406fd5c3f865a5e08c89b625245a9c4e026438fd1a445e45e6a0d69c2749 - md5: 981082c1cc262f514a5a2cf37cab9b81 + size: 663344 + timestamp: 1773854035739 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libnghttp2-1.68.1-hd3077d7_0.conda + sha256: 13782715b9eeebc4ad16d36e84ca569d1495e3516aea3fe546a32caa0a597d82 + md5: be5f0f007a4500a226ef001115535a3d depends: - - c-ares >=1.34.5,<2.0a0 + - c-ares >=1.34.6,<2.0a0 - libev >=4.33,<4.34.0a0 - libev >=4.33,<5.0a0 - libgcc >=14 - libstdcxx >=14 - libzlib >=1.3.1,<2.0a0 - - openssl >=3.5.2,<4.0a0 + - openssl >=3.5.5,<4.0a0 license: MIT license_family: MIT purls: [] - size: 728661 - timestamp: 1756835019535 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hd590300_0.conda - sha256: 26d77a3bb4dceeedc2a41bd688564fe71bf2d149fdcf117049970bc02ff1add6 - md5: 30fd6e37fe21f86f4bd26d6ee73eeec7 + size: 726928 + timestamp: 1773854039807 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda + sha256: 927fe72b054277cde6cb82597d0fcf6baf127dcbce2e0a9d8925a68f1265eef5 + md5: d864d34357c3b65a4b731f78c0801dc4 depends: - - libgcc-ng >=12 + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 license: LGPL-2.1-only license_family: GPL purls: [] - size: 33408 - timestamp: 1697359010159 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libnsl-2.0.1-h31becfc_0.conda - sha256: fd18c2b75d7411096428d36a70b36b1a17e31f7b8956b6905d145792d49e97f8 - md5: c14f32510f694e3185704d89967ec422 + size: 33731 + timestamp: 1750274110928 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libnsl-2.0.1-h86ecc28_1.conda + sha256: c0dc4d84198e3eef1f37321299e48e2754ca83fd12e6284754e3cb231357c3a5 + md5: d5d58b2dc3e57073fe22303f5fed4db7 depends: - - libgcc-ng >=12 + - libgcc >=13 license: LGPL-2.1-only license_family: GPL purls: [] - size: 34501 - timestamp: 1697358973269 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.49.1-hee588c1_2.conda - sha256: a086289bf75c33adc1daed3f1422024504ffb5c3c8b3285c49f025c29708ed16 - md5: 962d6ac93c30b1dfc54c9cccafd1003e + size: 34831 + timestamp: 1750274211 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.53.1-h0c1763c_0.conda + sha256: 54cdcd3214313b62c2a8ee277e6f42150d9b748264c1b70d958bf735e420ef8d + md5: 7dc38adcbf71e6b38748e919e16e0dce depends: - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - libzlib >=1.3.1,<2.0a0 - license: Unlicense + - libgcc >=14 + - libzlib >=1.3.2,<2.0a0 + license: blessing purls: [] - size: 918664 - timestamp: 1742083674731 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libsqlite-3.49.1-h5eb1b54_2.conda - sha256: c0eb05c6db32b52cc80e06a2badfa11fbaa66b49f1e7cff77aa691b74a294dcc - md5: 7c45959e187fd3313f9f1734464baecc + size: 954962 + timestamp: 1777986471789 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libsqlite-3.53.1-h022381a_0.conda + sha256: ad03b7d8e4d08001f0df88ee7a56108bb35bae4795a42b9a04cc1abfa822bd07 + md5: 2ec1119217d8f0d086e9a62f3cb0e5ea depends: - - libgcc >=13 - - libzlib >=1.3.1,<2.0a0 - license: Unlicense + - libgcc >=14 + - libzlib >=1.3.2,<2.0a0 + license: blessing purls: [] - size: 916419 - timestamp: 1742083699438 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.1-hf672d98_0.conda - sha256: 0407ac9fda2bb67e11e357066eff144c845801d00b5f664efbc48813af1e7bb9 - md5: be2de152d8073ef1c01b7728475f2fe7 + size: 955361 + timestamp: 1777986487553 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.1-hcf80075_0.conda + sha256: fa39bfd69228a13e553bd24601332b7cfeb30ca11a3ca50bb028108fe90a7661 + md5: eecce068c7e4eddeb169591baac20ac4 depends: - __glibc >=2.17,<3.0.a0 - libgcc >=13 - libzlib >=1.3.1,<2.0a0 - - openssl >=3.4.0,<4.0a0 + - openssl >=3.5.0,<4.0a0 license: BSD-3-Clause license_family: BSD purls: [] - size: 304278 - timestamp: 1732349402869 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libssh2-1.11.1-ha41c0db_0.conda - sha256: 40f2af5357457546bd11cd64a3b9043d83865180f65ce602515c35f353be35c7 - md5: aeffe03c0e598f015aab08dbb04f6ee4 + size: 304790 + timestamp: 1745608545575 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libssh2-1.11.1-h18c354c_0.conda + sha256: 1e289bcce4ee6a5817a19c66e296f3c644dcfa6e562e5c1cba807270798814e7 + md5: eecc495bcfdd9da8058969656f916cc2 depends: - libgcc >=13 - libzlib >=1.3.1,<2.0a0 - - openssl >=3.4.0,<4.0a0 + - openssl >=3.5.0,<4.0a0 license: BSD-3-Clause license_family: BSD purls: [] - size: 311577 - timestamp: 1732349396421 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-14.2.0-h8f9b012_2.conda - sha256: 8f5bd92e4a24e1d35ba015c5252e8f818898478cb3bc50bd8b12ab54707dc4da - md5: a78c856b6dc6bf4ea8daeb9beaaa3fb0 + size: 311396 + timestamp: 1745609845915 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_19.conda + sha256: dff1058c76ec6b8759e41cefa2508162d00e4a5e6721aa68ec3fd10094e702dc + md5: 5794b3bdc38177caf969dabd3af08549 depends: - __glibc >=2.17,<3.0.a0 - - libgcc 14.2.0 h767d61c_2 - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL - purls: [] - size: 3884556 - timestamp: 1740240685253 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libstdcxx-14.2.0-h3f4de04_2.conda - sha256: c30a74bc996013907f6d9f344da007c26d98ef9a0831151cd50aece3125c45d5 - md5: eadee2cda99697e29411c1013c187b92 - depends: - - libgcc 14.2.0 he277a41_2 - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL - purls: [] - size: 3810779 - timestamp: 1740241094774 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-14.2.0-h4852527_2.conda - sha256: e86f38b007cf97cc2c67cd519f2de12a313c4ee3f5ef11652ad08932a5e34189 - md5: c75da67f045c2627f59e6fcb5f4e3a9b - depends: - - libstdcxx 14.2.0 h8f9b012_2 + - libgcc 15.2.0 he0feb66_19 + constrains: + - libstdcxx-ng ==15.2.0=*_19 license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL purls: [] - size: 53830 - timestamp: 1740240722530 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libstdcxx-ng-14.2.0-hf1166c9_2.conda - sha256: 0107886ead6f255956d8e520f8dff260f9ab3d0d51512f18c52710c406e4b2df - md5: c934c1fddad582fcc385b608eb06a70c + size: 5852044 + timestamp: 1778269036376 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libstdcxx-15.2.0-hef695bb_19.conda + sha256: 1dadc45e599f510dd5f97141dddcdbb9844d9f1430c1f3a38075cf1c58f87b4e + md5: 543fbc8d71f2a0baf04cf88ce96cb8bb depends: - - libstdcxx 14.2.0 h3f4de04_2 + - libgcc 15.2.0 h8acb6b2_19 + constrains: + - libstdcxx-ng ==15.2.0=*_19 license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL purls: [] - size: 53715 - timestamp: 1740241126343 + size: 5546559 + timestamp: 1778268777463 - conda: https://conda.anaconda.org/conda-forge/linux-64/libunistring-0.9.10-h7f98852_0.tar.bz2 sha256: e88c45505921db29c08df3439ddb7f771bbff35f95e7d3103bf365d5d6ce2a6d md5: 7245a044b4a1980ed83196176b78b73a @@ -3228,47 +3335,48 @@ packages: purls: [] size: 1409624 timestamp: 1626959749923 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda - sha256: 787eb542f055a2b3de553614b25f09eefb0a0931b0c87dbcce6efdfd92f04f18 - md5: 40b61aab5c7ba9ff276c41cfffe6b80b +- conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42-h5347b49_0.conda + sha256: bc1b08c92626c91500fd9f26f2c797f3eb153b627d53e9c13cd167f1e12b2829 + md5: 38ffe67b78c9d4de527be8315e5ada2c depends: - - libgcc-ng >=12 + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 license: BSD-3-Clause license_family: BSD purls: [] - size: 33601 - timestamp: 1680112270483 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libuuid-2.38.1-hb4cce97_0.conda - sha256: 616277b0c5f7616c2cdf36f6c316ea3f9aa5bb35f2d4476a349ab58b9b91675f - md5: 000e30b09db0b7c775b21695dff30969 + size: 40297 + timestamp: 1775052476770 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libuuid-2.42-h1022ec0_0.conda + sha256: 7d427edf58c702c337bf62bc90f355b7fc374a65fd9f70ea7a490f13bb76b1b9 + md5: a0b5de740d01c390bdbb46d7503c9fab depends: - - libgcc-ng >=12 + - libgcc >=14 license: BSD-3-Clause license_family: BSD purls: [] - size: 35720 - timestamp: 1680113474501 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libuv-1.50.0-hb9d3cd8_0.conda - sha256: b4a8890023902aef9f1f33e3e35603ad9c2f16c21fdb58e968fa6c1bd3e94c0b - md5: 771ee65e13bc599b0b62af5359d80169 + size: 43567 + timestamp: 1775052485727 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libuv-1.51.0-hb03c661_1.conda + sha256: c180f4124a889ac343fc59d15558e93667d894a966ec6fdb61da1604481be26b + md5: 0f03292cc56bf91a077a134ea8747118 depends: - __glibc >=2.17,<3.0.a0 - - libgcc >=13 + - libgcc >=14 license: MIT license_family: MIT purls: [] - size: 891272 - timestamp: 1737016632446 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libuv-1.50.0-h86ecc28_0.conda - sha256: 67914c7f171d343059144d804c2f17fcd621a94e45f179a0fd843b8c1618823e - md5: 915db044076cbbdffb425170deb4ce38 + size: 895108 + timestamp: 1753948278280 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libuv-1.51.0-he30d5cf_1.conda + sha256: 7a0fb5638582efc887a18b7d270b0c4a6f6e681bf401cab25ebafa2482569e90 + md5: 8e62bf5af966325ee416f19c6f14ffa3 depends: - - libgcc >=13 + - libgcc >=14 license: MIT license_family: MIT purls: [] - size: 621056 - timestamp: 1737016626950 + size: 629238 + timestamp: 1753948296190 - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda sha256: 6ae68e0b86423ef188196fff6207ed0c8195dd84273cb5623b85aa08033a410c md5: 5aa797f8787fe7a17d1b0821485b5adc @@ -3287,31 +3395,28 @@ packages: purls: [] size: 114269 timestamp: 1702724369203 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda - sha256: d4bfe88d7cb447768e31650f06257995601f89076080e76df55e3112d4e47dc4 - md5: edb0dca6bc32e4f4789199455a1dbeb8 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda + sha256: 55044c403570f0dc26e6364de4dc5368e5f3fc7ff103e867c487e2b5ab2bcda9 + md5: d87ff7921124eccd67248aa483c23fec depends: - __glibc >=2.17,<3.0.a0 - - libgcc >=13 constrains: - - zlib 1.3.1 *_2 + - zlib 1.3.2 *_2 license: Zlib license_family: Other purls: [] - size: 60963 - timestamp: 1727963148474 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libzlib-1.3.1-h86ecc28_2.conda - sha256: 5a2c1eeef69342e88a98d1d95bff1603727ab1ff4ee0e421522acd8813439b84 - md5: 08aad7cbe9f5a6b460d0976076b6ae64 - depends: - - libgcc >=13 + size: 63629 + timestamp: 1774072609062 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libzlib-1.3.2-hdc9db2a_2.conda + sha256: eb111e32e5a7313a5bf799c7fb2419051fa2fe7eff74769fac8d5a448b309f7f + md5: 502006882cf5461adced436e410046d1 constrains: - - zlib 1.3.1 *_2 + - zlib 1.3.2 *_2 license: Zlib license_family: Other purls: [] - size: 66657 - timestamp: 1727963199518 + size: 69833 + timestamp: 1774072605429 - pypi: https://files.pythonhosted.org/packages/b4/de/88b3be5c31b22333b3ca2f6ff1de4e863d8fe45aaea7485f591970ec1d3e/linkify_it_py-2.1.0-py3-none-any.whl name: linkify-it-py version: 2.1.0 @@ -3336,19 +3441,20 @@ packages: name: lockfile version: 0.12.2 sha256: 6c3cb24f344923d30b2785d5ad75182c8ea7ac1b6171b08657258ec7429d50fa -- conda: https://conda.anaconda.org/conda-forge/noarch/mako-1.3.9-pyhd8ed1ab_0.conda - sha256: 56ac22e0800b44600662de49f8bc241b2d785820e44d96eebb6eae7e072c8a99 - md5: 422113c902cc5181ccaafbb4b827e492 +- conda: https://conda.anaconda.org/conda-forge/noarch/mako-1.3.12-pyhcf101f3_0.conda + sha256: d06d02574be3892020262464b49360a749c1d448ed9f0de52fe8a08bc1483261 + md5: a73036dabdd6dfe9679ed893baa8b230 depends: + - python >=3.10 - importlib-metadata - markupsafe >=0.9.2 - - python >=3.9 + - python license: MIT license_family: MIT purls: - pkg:pypi/mako?source=hash-mapping - size: 67008 - timestamp: 1738719687521 + size: 72185 + timestamp: 1777410001911 - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl name: markdown version: 3.10.2 @@ -3398,12 +3504,12 @@ packages: - pytest-timeout ; extra == 'testing' - requests ; extra == 'testing' requires_python: '>=3.10' -- conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.2-py310h89163eb_1.conda - sha256: 0bed20ec27dcbcaf04f02b2345358e1161fb338f8423a4ada1cf0f4d46918741 - md5: 8ce3f0332fd6de0d737e2911d329523f +- conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py310h3406613_1.conda + sha256: 9f3c34f8a7a8dcfed64221a2e19bbe0094ab2c6df7c029b7df713e52c9c9f229 + md5: 671afe636d2a97759804723f5afc22e0 depends: - __glibc >=2.17,<3.0.a0 - - libgcc >=13 + - libgcc >=14 - python >=3.10,<3.11.0a0 - python_abi 3.10.* *_cp310 constrains: @@ -3412,13 +3518,13 @@ packages: license_family: BSD purls: - pkg:pypi/markupsafe?source=hash-mapping - size: 23091 - timestamp: 1733219814479 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/markupsafe-3.0.2-py310h66848f9_1.conda - sha256: e27cf7530d5ea1eb405a7d2f3d13fa0eda6f92e4d2c62b3a6a171c9943250383 - md5: f0f91c0ef87d60f043e00b540293795d + size: 23899 + timestamp: 1772445369460 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/markupsafe-3.0.3-py310h3b5aacf_1.conda + sha256: 3809439f3f7a28893314ac09aeeb04a7663efcfbb48281f9e65a916d5c7b764a + md5: 3a8d4c69d626abcfaede3f89df2599f8 depends: - - libgcc >=13 + - libgcc >=14 - python >=3.10,<3.11.0a0 - python_abi 3.10.* *_cp310 constrains: @@ -3427,12 +3533,12 @@ packages: license_family: BSD purls: - pkg:pypi/markupsafe?source=hash-mapping - size: 23294 - timestamp: 1733220959789 -- pypi: https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl + size: 24568 + timestamp: 1772446363441 +- pypi: https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl name: matplotlib-inline - version: 0.2.1 - sha256: d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76 + version: 0.2.2 + sha256: 3c821cf1c209f59fb2d2d64abbf5b23b67bcb2210d663f9918dd851c6da1fcf6 requires_dist: - traitlets - flake8 ; extra == 'test' @@ -3440,11 +3546,12 @@ packages: - nbval ; extra == 'test' - notebook ; extra == 'test' - pytest ; extra == 'test' + - matplotlib ; extra == 'test' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/71/d6/48f5b9e44e2e760855d7b489b1317cd7620e82dcb73197961e5cc1391348/mdit_py_plugins-0.6.0-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl name: mdit-py-plugins - version: 0.6.0 - sha256: f7e7a25d8b616fee99cb1e330da73451d11a8061baf39bb9663ab9ce0e005b90 + version: 0.6.1 + sha256: 214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d requires_dist: - markdown-it-py>=2.0.0,<5.0.0 - pre-commit ; extra == 'code-style' @@ -3483,34 +3590,36 @@ packages: version: 1.1.2 sha256: 365c0bbe981a27d8932da71af63ef86acc59ed5c01ad929e09a0b88c6294e28a requires_python: '>=3.9' -- conda: https://conda.anaconda.org/conda-forge/linux-64/multidict-6.2.0-py310h89163eb_0.conda - sha256: dc678195b6d5e3beae5a0df107bcc310ecc7e93e0ac9d67b57ee9eb729090760 - md5: b58e297cc037aba6f32edef76fd0e49a +- conda: https://conda.anaconda.org/conda-forge/linux-64/multidict-6.7.1-py310h3406613_0.conda + sha256: 7566d0cd317ffb09374d0fdcfd921c8fe14702d551d159173b582c996b123fca + md5: 60069e0f230489d6f7119009696321c7 depends: - __glibc >=2.17,<3.0.a0 - - libgcc >=13 + - libgcc >=14 - python >=3.10,<3.11.0a0 - python_abi 3.10.* *_cp310 - - typing-extensions + - typing-extensions >=4.1.0 license: Apache-2.0 + license_family: APACHE purls: - pkg:pypi/multidict?source=hash-mapping - size: 61111 - timestamp: 1742308417374 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/multidict-6.2.0-py310heeae437_0.conda - sha256: a4a993ac71661587bd537e850740c807d35f95e4e814e36434fd6636309559c3 - md5: 179f90635b995a32b5fd677971c67e42 + size: 91663 + timestamp: 1771611110254 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/multidict-6.7.1-py310h2d8da20_0.conda + sha256: 56d7e436569e1f9839b13f56c8cca86b2250f6de55974c053ed0032c7bb60fa9 + md5: ac6a85104ecc7d26ffc1680648803b2c depends: - - libgcc >=13 + - libgcc >=14 - python >=3.10,<3.11.0a0 - python >=3.10,<3.11.0a0 *_cpython - python_abi 3.10.* *_cp310 - - typing-extensions + - typing-extensions >=4.1.0 license: Apache-2.0 + license_family: APACHE purls: - pkg:pypi/multidict?source=hash-mapping - size: 62769 - timestamp: 1742308314182 + size: 93522 + timestamp: 1771610842915 - pypi: https://files.pythonhosted.org/packages/c7/e1/68c2256b69a314eba133673377ba9118c356f6342a0c02b61de449cf2bf2/narwhals-2.21.0-py3-none-any.whl name: narwhals version: 2.21.0 @@ -3641,25 +3750,25 @@ packages: - pytest ; extra == 'test' - testpath ; extra == 'test' requires_python: '>=3.8' -- conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda - sha256: 3fde293232fa3fca98635e1167de6b7c7fda83caf24b9d6c91ec9eefb4f4d586 - md5: 47e340acb35de30501a76c7c799c41d7 +- conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.6-hdb14827_0.conda + sha256: fc89f74bbe362fb29fa3c037697a89bec140b346a2469a90f7936d1d7ea4d8a3 + md5: fc21868a1a5aacc937e7a18747acb8a5 depends: - __glibc >=2.17,<3.0.a0 - - libgcc >=13 + - libgcc >=14 license: X11 AND BSD-3-Clause purls: [] - size: 891641 - timestamp: 1738195959188 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ncurses-6.5-ha32ae93_3.conda - sha256: 91cfb655a68b0353b2833521dc919188db3d8a7f4c64bea2c6a7557b24747468 - md5: 182afabe009dc78d8b73100255ee6868 + size: 918956 + timestamp: 1777422145199 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ncurses-6.6-hf8d1292_0.conda + sha256: 369db85c5cd8d99dde364ce70725d76511d9c8199e5b820c740414091bf5bcca + md5: b2a43456aa56fe80c2477a5094899eff depends: - - libgcc >=13 + - libgcc >=14 license: X11 AND BSD-3-Clause purls: [] - size: 926034 - timestamp: 1738196018799 + size: 960036 + timestamp: 1777422174534 - pypi: https://files.pythonhosted.org/packages/d3/84/a2d7ced096d0c0c16d663fec2da68a13312e2b621fad19fe324f943c1f55/nebari_jupyterhub_theme-2024.7.1-py3-none-any.whl name: nebari-jupyterhub-theme version: 2024.7.1 @@ -3672,40 +3781,52 @@ packages: version: 1.6.0 sha256: 87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c requires_python: '>=3.5' -- conda: https://conda.anaconda.org/conda-forge/linux-64/nodejs-22.13.0-hf235a45_0.conda - sha256: 925ea8839d6f26d0eb4204675b98a862803a9a9657fd36a4a22c4c29a479a911 - md5: 1f9efd96347aa008bd2c735d7d88fc75 +- conda: https://conda.anaconda.org/conda-forge/linux-64/nodejs-24.14.1-h3d65ac4_0.conda + sha256: 116de7e13c8211217ffcb4ed333202052e6c1d565c5ee439bbaeb97c2aeb8271 + md5: fa4e76aac348ef9c27e72c79b02833fc depends: + - libgcc >=14 - __glibc >=2.28,<3.0.a0 - - icu >=75.1,<76.0a0 - - libgcc >=13 - - libstdcxx >=13 - - libuv >=1.50.0,<2.0a0 - - libzlib >=1.3.1,<2.0a0 - - openssl >=3.4.1,<4.0a0 - - zlib + - libstdcxx >=14 + - zstd >=1.5.7,<1.6.0a0 + - libbrotlicommon >=1.2.0,<1.3.0a0 + - libbrotlienc >=1.2.0,<1.3.0a0 + - libbrotlidec >=1.2.0,<1.3.0a0 + - c-ares >=1.34.6,<2.0a0 + - libsqlite >=3.52.0,<4.0a0 + - libzlib >=1.3.2,<2.0a0 + - libuv >=1.51.0,<2.0a0 + - libnghttp2 >=1.68.1,<2.0a0 + - openssl >=3.5.5,<4.0a0 + - icu >=78.3,<79.0a0 license: MIT license_family: MIT purls: [] - size: 21691794 - timestamp: 1741809786920 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/nodejs-22.13.0-h8374285_0.conda - sha256: 7878e84a0162041c7c45814832f1635ea0b4454481af619a20a3be291744c032 - md5: 3a81ef0e4e847685774c35bbf42598e2 + size: 17575783 + timestamp: 1774517834182 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/nodejs-24.14.1-he5f9b4c_0.conda + sha256: 537c9938cc6a180e1aff7f5cd737f3f248feff8c2a4ec831324ab9797adc93f1 + md5: 8637b0572ebf0d51e431a2db58286c01 depends: - __glibc >=2.28,<3.0.a0 - - icu >=75.1,<76.0a0 - - libgcc >=13 - - libstdcxx >=13 - - libuv >=1.50.0,<2.0a0 - - libzlib >=1.3.1,<2.0a0 - - openssl >=3.4.0,<4.0a0 - - zlib + - libgcc >=14 + - libstdcxx >=14 + - icu >=78.3,<79.0a0 + - libnghttp2 >=1.68.1,<2.0a0 + - openssl >=3.5.5,<4.0a0 + - libbrotlicommon >=1.2.0,<1.3.0a0 + - libbrotlienc >=1.2.0,<1.3.0a0 + - libbrotlidec >=1.2.0,<1.3.0a0 + - c-ares >=1.34.6,<2.0a0 + - zstd >=1.5.7,<1.6.0a0 + - libzlib >=1.3.2,<2.0a0 + - libuv >=1.51.0,<2.0a0 + - libsqlite >=3.52.0,<4.0a0 license: MIT license_family: MIT purls: [] - size: 22156450 - timestamp: 1737394666729 + size: 23415087 + timestamp: 1774518161811 - pypi: https://files.pythonhosted.org/packages/e9/d6/1fd0646b9bbd9efbb0b8ae21b2325fbef515769a5621c03e31d8eb8da587/notebook-7.5.6-py3-none-any.whl name: notebook version: 7.5.6 @@ -3774,9 +3895,9 @@ packages: - pkg:pypi/oauthenticator?source=hash-mapping size: 55946 timestamp: 1710943235071 -- conda: https://conda.anaconda.org/conda-forge/noarch/oauthlib-3.2.2-pyhd8ed1ab_1.conda - sha256: bec65607d36759e85aab2331ff7f056cb32be0bca92ee2b955aea3306330bd1b - md5: bf5f2c90d503d43a8c45cedf766b4b8e +- conda: https://conda.anaconda.org/conda-forge/noarch/oauthlib-3.3.1-pyhd8ed1ab_0.conda + sha256: dfa8222df90736fa13f8896f5a573a50273af8347542d412c3bd1230058e56a5 + md5: d4f3f31ee39db3efecb96c0728d4bdbf depends: - blinker - cryptography @@ -3786,11 +3907,11 @@ packages: license_family: BSD purls: - pkg:pypi/oauthlib?source=hash-mapping - size: 97604 - timestamp: 1733752957557 -- conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.1-h35e630c_1.conda - sha256: 44c877f8af015332a5d12f5ff0fb20ca32f896526a7d0cdb30c769df1144fb5c - md5: f61eb8cd60ff9057122a3d338b99c00f + size: 102059 + timestamp: 1750415349440 +- conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.2-h35e630c_0.conda + sha256: c0ef482280e38c71a08ad6d71448194b719630345b0c9c60744a2010e8a8e0cb + md5: da1b85b6a87e141f5140bb9924cecab0 depends: - __glibc >=2.17,<3.0.a0 - ca-certificates @@ -3798,19 +3919,19 @@ packages: license: Apache-2.0 license_family: Apache purls: [] - size: 3164551 - timestamp: 1769555830639 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/openssl-3.6.1-h546c87b_1.conda - sha256: 7f8048c0e75b2620254218d72b4ae7f14136f1981c5eb555ef61645a9344505f - md5: 25f5885f11e8b1f075bccf4a2da91c60 + size: 3167099 + timestamp: 1775587756857 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/openssl-3.6.2-h546c87b_0.conda + sha256: 348cb74c1530ac241215d047ef65d134cf797af935c97a68655319362b7e6a01 + md5: 3b129669089e4d6a5c6871dbb4669b99 depends: - ca-certificates - libgcc >=14 license: Apache-2.0 license_family: Apache purls: [] - size: 3692030 - timestamp: 1769557678657 + size: 3706406 + timestamp: 1775589602258 - pypi: https://files.pythonhosted.org/packages/2c/ab/fc8290c6a4c722e5514d80f62b2dc4c4df1a68a41d1364e625c35990fcf3/overrides-7.7.0-py3-none-any.whl name: overrides version: 7.7.0 @@ -3818,17 +3939,18 @@ packages: requires_dist: - typing ; python_full_version < '3.5' requires_python: '>=3.6' -- conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda - sha256: da157b19bcd398b9804c5c52fc000fcb8ab0525bdb9c70f95beaa0bb42f85af1 - md5: 3bfed7e6228ebf2f7b9eaa47f1b4e2aa +- conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.2-pyhc364b38_0.conda + sha256: 3906abfb6511a3bb309e39b9b1b7bc38f50a723971de2395489fd1f379255890 + md5: 4c06a92e74452cfa53623a81592e8934 depends: - python >=3.8 + - python license: Apache-2.0 license_family: APACHE purls: - pkg:pypi/packaging?source=hash-mapping - size: 60164 - timestamp: 1733203368787 + size: 91574 + timestamp: 1777103621679 - conda: https://conda.anaconda.org/conda-forge/noarch/pamela-1.2.0-pyhd8ed1ab_1.conda sha256: 41b074a35b210b3395ccd10d30c301c0f7c65150353820f3a6a7d2bf8be5beaa md5: a3a069b6dbf63e1a635f3feeffdaeb4e @@ -4282,16 +4404,6 @@ packages: - pytest-cov ; extra == 'testing' - wheel ; extra == 'testing' requires_python: '>=3.8' -- conda: https://conda.anaconda.org/conda-forge/noarch/pkgutil-resolve-name-1.3.10-pyhd8ed1ab_2.conda - sha256: adb2dde5b4f7da70ae81309cce6188ed3286ff280355cf1931b45d91164d2ad8 - md5: 5a5870a74432aa332f7d32180633ad05 - depends: - - python >=3.9 - license: MIT AND PSF-2.0 - purls: - - pkg:pypi/pkgutil-resolve-name?source=hash-mapping - size: 10693 - timestamp: 1733344619659 - pypi: https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl name: platformdirs version: 4.9.6 @@ -4316,17 +4428,17 @@ packages: - pytest-benchmark ; extra == 'testing' - coverage ; extra == 'testing' requires_python: '>=3.9' -- conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.21.1-pyhd8ed1ab_0.conda - sha256: bc8f00d5155deb7b47702cb8370f233935704100dbc23e30747c161d1b6cf3ab - md5: 3e01e386307acc60b2f89af0b2e161aa +- conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.25.0-pyhd8ed1ab_0.conda + sha256: 4d7ec90d4f9c1f3b4a50623fefe4ebba69f651b102b373f7c0e9dbbfa43d495c + md5: a11ab1f31af799dd93c3a39881528884 depends: - - python >=3.9 + - python >=3.10 license: Apache-2.0 license_family: Apache purls: - pkg:pypi/prometheus-client?source=hash-mapping - size: 49002 - timestamp: 1733327434163 + size: 57113 + timestamp: 1775771465170 - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl name: prompt-toolkit version: 3.0.52 @@ -4334,9 +4446,9 @@ packages: requires_dist: - wcwidth requires_python: '>=3.8' -- conda: https://conda.anaconda.org/conda-forge/linux-64/propcache-0.2.1-py310h89163eb_1.conda - sha256: ed06b08001335e1959d56c25a1c31871df0a56206d4c64b7309d015682dca08f - md5: ff4090c5ecf2e74e011c7c2404090ac5 +- conda: https://conda.anaconda.org/conda-forge/linux-64/propcache-0.3.1-py310h89163eb_0.conda + sha256: 3dbf885bb1eb0e7a5eb3779165517abdb98d53871b36690041f6a366cc501738 + md5: e768486f2be3f50126bf9a54331221d1 depends: - __glibc >=2.17,<3.0.a0 - libgcc >=13 @@ -4346,11 +4458,11 @@ packages: license_family: APACHE purls: - pkg:pypi/propcache?source=hash-mapping - size: 52716 - timestamp: 1737635856694 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/propcache-0.2.1-py310heeae437_1.conda - sha256: 8b621e6f826ec41e186bd1e4e3eed8da754cd93e41d240540f40407b47cb10d3 - md5: 3a50948d5933233e1312d6e6e4e1b71d + size: 53576 + timestamp: 1744525075233 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/propcache-0.3.1-py310heeae437_0.conda + sha256: 92455cf21615f5dea64067e389cae915cd0acf10fc55c14423722ba177edfa96 + md5: aae5f209a10b56879c453014e04140cd depends: - libgcc >=13 - python >=3.10,<3.11.0a0 @@ -4360,36 +4472,36 @@ packages: license_family: APACHE purls: - pkg:pypi/propcache?source=hash-mapping - size: 52028 - timestamp: 1737635772514 -- conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.0.0-py310ha75aee5_0.conda - sha256: 31e46270c73cac2b24a7f3462ca03eb39f21cbfdb713b0d41eb61c00867eabe9 - md5: da7d592394ff9084a23f62a1186451a2 + size: 53390 + timestamp: 1744824248180 +- conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.2.2-py310h139afa4_0.conda + sha256: 3a6d46033ebad3e69ded3f76852b9c378c2cff632f57421b5926c6add1bae475 + md5: d210342acdb8e3ca6434295497c10b7c depends: + - python + - libgcc >=14 - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - python >=3.10,<3.11.0a0 - python_abi 3.10.* *_cp310 license: BSD-3-Clause license_family: BSD purls: - - pkg:pypi/psutil?source=compressed-mapping - size: 354476 - timestamp: 1740663252954 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/psutil-7.0.0-py310ha766c32_0.conda - sha256: bb4c160157b4bc274ad7f2ea1193d1025300c05ea3f626f6920bb2f9d5969f3d - md5: 86b79b1ae52abe6dcf33fae9d4555bf1 + - pkg:pypi/psutil?source=hash-mapping + size: 179015 + timestamp: 1769678154886 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/psutil-7.2.2-py310hef25091_0.conda + sha256: c66b0f7f0e4fdc3b5b7c138246426a4a6b32af3a71bef2605264d488fabc4c45 + md5: 4f824cd2641bdafd0865edd5a85300d2 depends: - - libgcc >=13 - - python >=3.10,<3.11.0a0 - - python >=3.10,<3.11.0a0 *_cpython + - python + - python 3.10.* *_cpython + - libgcc >=14 - python_abi 3.10.* *_cp310 license: BSD-3-Clause license_family: BSD purls: - pkg:pypi/psutil?source=hash-mapping - size: 356360 - timestamp: 1740663310611 + size: 183737 + timestamp: 1769678160656 - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl name: ptyprocess version: 0.7.0 @@ -4400,29 +4512,30 @@ packages: sha256: 1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0 requires_dist: - pytest ; extra == 'tests' -- conda: https://conda.anaconda.org/conda-forge/noarch/pyasn1-0.6.1-pyhd8ed1ab_2.conda - sha256: d06051df66e9ab753683d7423fcef873d78bb0c33bd112c3d5be66d529eddf06 - md5: 09bb17ed307ad6ab2fd78d32372fdd4e +- conda: https://conda.anaconda.org/conda-forge/noarch/pyasn1-0.6.3-pyhcf101f3_0.conda + sha256: 6fd53b7a2793404aef62313ff2fcfef0c661d6b71de90ef3d38c0908249eea76 + md5: f5a488544d2eb37f46b3bebf1f378337 depends: - - python >=3.9 + - python >=3.10 + - python license: BSD-2-Clause license_family: BSD purls: - pkg:pypi/pyasn1?source=hash-mapping - size: 62230 - timestamp: 1733217699113 -- conda: https://conda.anaconda.org/conda-forge/noarch/pyasn1-modules-0.4.1-pyhd8ed1ab_1.conda - sha256: 565e961fce215ccf14f863c3030eda5b83014489679d27166ff97144bf977810 - md5: 1c6476fdb96e6c3db6c3f7693cdba78e + size: 66593 + timestamp: 1773729387446 +- conda: https://conda.anaconda.org/conda-forge/noarch/pyasn1-modules-0.4.2-pyhd8ed1ab_0.conda + sha256: 5495061f5d3d6b82b74d400273c586e7c1f1700183de1d2d1688e900071687cb + md5: c689b62552f6b63f32f3322e463f3805 depends: - - pyasn1 >=0.4.6,<0.7.0 + - pyasn1 >=0.6.1,<0.7.0 - python >=3.9 license: BSD-2-Clause license_family: BSD purls: - pkg:pypi/pyasn1-modules?source=hash-mapping - size: 95825 - timestamp: 1733324693664 + size: 95990 + timestamp: 1743436137965 - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda sha256: 79db7928d13fab2d892592223d7570f5061c192f27b9febd1a418427b719acc6 md5: 12c566707c80111f9799308d9e265aef @@ -4435,85 +4548,86 @@ packages: - pkg:pypi/pycparser?source=hash-mapping size: 110100 timestamp: 1733195786147 -- conda: https://conda.anaconda.org/conda-forge/linux-64/pycurl-7.45.6-py310h6811363_0.conda - sha256: 0570f2a814dd7e33bad3a7bdcf3199ea72169647b923cfc9b602ff3eacc763d8 - md5: 24873c2e43612f18accd9ca48a4dfc74 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pycurl-7.46.0-py310h0aba9cc_0.conda + sha256: c428e4ca0466fecd00a9367acc2102fa0cf9877dbf9367fdd97e053a22ae1e00 + md5: b2d19f40db53d5ce2626482cd9aea846 depends: + - python - __glibc >=2.17,<3.0.a0 - - libcurl >=8.12.1,<9.0a0 - - libgcc >=13 - - openssl >=3.4.1,<4.0a0 - - python >=3.10,<3.11.0a0 + - libgcc >=14 + - libcurl >=8.20.0,<9.0a0 - python_abi 3.10.* *_cp310 + - openssl >=3.5.6,<4.0a0 license: LGPL-2.1-or-later OR curl purls: - pkg:pypi/pycurl?source=hash-mapping - size: 78585 - timestamp: 1741334051811 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pycurl-7.45.6-py310h25e8102_0.conda - sha256: fca143e1dce8cc609a21a5001d74b6fbec2eac75edb0aa99a009f1867bd2d8c9 - md5: 67e3fe385037f4c0e62236ca7470b062 + size: 128776 + timestamp: 1777531716056 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pycurl-7.46.0-py310h48fd690_0.conda + sha256: 0452b8183a45912c6411d4f01a38b599adc8e123f33bf00de62752bf2fe67f03 + md5: 4cd6d760d7ed97d5acfe2560e740e836 depends: - - libcurl >=8.12.1,<9.0a0 - - libgcc >=13 - - openssl >=3.4.1,<4.0a0 - - python >=3.10,<3.11.0a0 - - python >=3.10,<3.11.0a0 *_cpython + - python + - python 3.10.* *_cpython + - libgcc >=14 + - libcurl >=8.20.0,<9.0a0 + - openssl >=3.5.6,<4.0a0 - python_abi 3.10.* *_cp310 license: LGPL-2.1-or-later OR curl purls: - pkg:pypi/pycurl?source=hash-mapping - size: 81251 - timestamp: 1741334128900 -- conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.10.6-pyh3cfb1c2_0.conda - sha256: 9a78801a28959edeb945e8270a4e666577b52fac0cf4e35f88cf122f73d83e75 - md5: c69f87041cf24dfc8cb6bf64ca7133c7 - depends: + size: 135574 + timestamp: 1777531728073 +- conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.13.4-pyhcf101f3_0.conda + sha256: 69700e31165df070e9716315e042196aa92525dae5deb5107785847ab9f4189f + md5: 729843edafc0899b3348bd3f19525b9d + depends: + - typing-inspection >=0.4.2 + - typing_extensions >=4.14.1 + - python >=3.10 - annotated-types >=0.6.0 - - pydantic-core 2.27.2 - - python >=3.9 - - typing-extensions >=4.6.1 - - typing_extensions >=4.12.2 + - pydantic-core ==2.46.4 + - python license: MIT license_family: MIT purls: - pkg:pypi/pydantic?source=hash-mapping - size: 296841 - timestamp: 1737761472006 -- conda: https://conda.anaconda.org/conda-forge/linux-64/pydantic-core-2.27.2-py310h505e2c1_0.conda - sha256: 6c58cdbb007f2dc8b0a8d96eacaba45bedf6ddfe9ed4558c40f899cb3438f3cb - md5: 3f804346d970a0343c46afc21cf5f16e + size: 346511 + timestamp: 1778103405862 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pydantic-core-2.46.4-py310hd8f68c5_0.conda + sha256: 0925c4329787c54e9103ac9ef92c02922bf58a46989eb7b072cf46ab6325e7cd + md5: 72935a417123f81985cc29b58ed26b3c depends: + - python + - typing-extensions >=4.6.0,!=4.7.0 - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - python >=3.10,<3.11.0a0 + - libgcc >=14 - python_abi 3.10.* *_cp310 - - typing-extensions >=4.6.0,!=4.7.0 constrains: - __glibc >=2.17 license: MIT license_family: MIT purls: - pkg:pypi/pydantic-core?source=hash-mapping - size: 1636318 - timestamp: 1734571799517 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pydantic-core-2.27.2-py310h04a307d_0.conda - sha256: d27397d6d51d0df725d92a8e9c4bb76fe055002ae3f010cd71351ec58ff8ffd4 - md5: 6c15ade3b7dd9604d65ed4ab2e0cf787 + size: 1878295 + timestamp: 1778084228512 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pydantic-core-2.46.4-py310h598bb90_0.conda + sha256: 2aa30bcee79dc5e4f978119559f38abaa119070598d3946aa79207d3211215a0 + md5: d9c2556e5d5b61da007f05a89a3f4741 depends: - - libgcc >=13 - - python >=3.10,<3.11.0a0 - - python >=3.10,<3.11.0a0 *_cpython - - python_abi 3.10.* *_cp310 + - python - typing-extensions >=4.6.0,!=4.7.0 + - python 3.10.* *_cpython + - libgcc >=14 + - python_abi 3.10.* *_cp310 constrains: - __glibc >=2.17 license: MIT license_family: MIT purls: - pkg:pypi/pydantic-core?source=hash-mapping - size: 1507348 - timestamp: 1734571930496 + size: 1771125 + timestamp: 1778084241252 - pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl name: pygments version: 2.20.0 @@ -4521,34 +4635,33 @@ packages: requires_dist: - colorama>=0.4.6 ; extra == 'windows-terminal' requires_python: '>=3.9' -- conda: https://conda.anaconda.org/conda-forge/noarch/pyjwt-2.12.1-pyhcf101f3_0.conda - sha256: 4279ee4cf2533fd17910ae7373159d9bee2492d8c50932ddc74dd27a70b15de4 - md5: b27a9f4eca2925036e43542488d3a804 +- conda: https://conda.anaconda.org/conda-forge/noarch/pyjwt-2.9.0-pyhd8ed1ab_1.conda + sha256: b6f47cd0737cb1f5aca10be771641466ec1a3be585382d44877140eb2cb2dd46 + md5: 5ba575830ec18d5c51c59f403310e2c7 depends: - - python >=3.10 - - typing_extensions >=4.0 - - python + - python >=3.8 constrains: - cryptography >=3.4.0 license: MIT license_family: MIT purls: - pkg:pypi/pyjwt?source=hash-mapping - size: 32247 - timestamp: 1773482160904 -- conda: https://conda.anaconda.org/conda-forge/noarch/pyopenssl-25.0.0-pyhd8ed1ab_0.conda - sha256: 18a487af2ae5e2c380a8bb3fe38da2b4dc3aa8d033aa75202442e1075e6f635b - md5: 195fbabc5cc805f2cc10cb881a19cf8b + size: 24346 + timestamp: 1722701382367 +- conda: https://conda.anaconda.org/conda-forge/noarch/pyopenssl-26.0.0-pyhcf101f3_0.conda + sha256: db1475010a893f3592132fbf03d99cfbf10822fb03f185898f3d014af485fdbd + md5: 5291776e59082b5244ab973a8fd66e8b depends: - - cryptography >=41.0.5,<45 - - python >=3.9 + - python >=3.10 + - cryptography >=46.0.0,<47 - typing-extensions >=4.9 + - python license: Apache-2.0 - license_family: Apache + license_family: APACHE purls: - pkg:pypi/pyopenssl?source=hash-mapping - size: 122758 - timestamp: 1737243471659 + size: 134272 + timestamp: 1774513012966 - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl name: pyproject-hooks version: 1.2.0 @@ -4566,71 +4679,72 @@ packages: - pkg:pypi/pysocks?source=hash-mapping size: 21085 timestamp: 1733217331982 -- conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.10.16-he725a3c_1_cpython.conda - build_number: 1 - sha256: 3f90a2d5062a73cd2dd8a0027718aee1db93f7975b9cfe529e2c9aeec2db262e - md5: b887811a901b3aa622a92caf03bc8917 +- conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.10.20-h3c07f61_0_cpython.conda + sha256: 8ff2ce308faf2588b69c65b120293f59a8f2577b772b34df4e817d220b09e081 + md5: 5d4e2b00d99feacd026859b7fa239dc0 depends: - __glibc >=2.17,<3.0.a0 - bzip2 >=1.0.8,<2.0a0 - ld_impl_linux-64 >=2.36.1 + - libexpat >=2.7.4,<3.0a0 - libffi >=3.4,<4.0a0 - - libgcc >=13 - - liblzma >=5.6.3,<6.0a0 + - libgcc >=14 + - liblzma >=5.8.2,<6.0a0 - libnsl >=2.0.1,<2.1.0a0 - - libsqlite >=3.47.0,<4.0a0 - - libuuid >=2.38.1,<3.0a0 + - libsqlite >=3.51.2,<4.0a0 + - libuuid >=2.41.3,<3.0a0 - libxcrypt >=4.4.36 - libzlib >=1.3.1,<2.0a0 - ncurses >=6.5,<7.0a0 - - openssl >=3.4.0,<4.0a0 - - readline >=8.2,<9.0a0 + - openssl >=3.5.5,<4.0a0 + - readline >=8.3,<9.0a0 - tk >=8.6.13,<8.7.0a0 - tzdata constrains: - python_abi 3.10.* *_cp310 license: Python-2.0 purls: [] - size: 25199631 - timestamp: 1733409331823 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/python-3.10.16-h57b00e1_1_cpython.conda - build_number: 1 - sha256: d8d75b16599a66c3dd2d55cc6f94d79d899a59da5369493471c5a3ee27629745 - md5: c4b3a08e4d6fc7b070720f75bc883b47 + size: 25455342 + timestamp: 1772729810280 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/python-3.10.20-h28be5d3_0_cpython.conda + sha256: 2f16794d69e058404cca633021b284e70253dfb76e4b314d9dbd448aa0354e84 + md5: 5d61c9a2acf7c675f7ae5f6cf51ffb0b depends: - bzip2 >=1.0.8,<2.0a0 - ld_impl_linux-aarch64 >=2.36.1 + - libexpat >=2.7.4,<3.0a0 - libffi >=3.4,<4.0a0 - - libgcc >=13 - - liblzma >=5.6.3,<6.0a0 + - libgcc >=14 + - liblzma >=5.8.2,<6.0a0 - libnsl >=2.0.1,<2.1.0a0 - - libsqlite >=3.47.0,<4.0a0 - - libuuid >=2.38.1,<3.0a0 + - libsqlite >=3.51.2,<4.0a0 + - libuuid >=2.41.3,<3.0a0 - libxcrypt >=4.4.36 - libzlib >=1.3.1,<2.0a0 - ncurses >=6.5,<7.0a0 - - openssl >=3.4.0,<4.0a0 - - readline >=8.2,<9.0a0 + - openssl >=3.5.5,<4.0a0 + - readline >=8.3,<9.0a0 - tk >=8.6.13,<8.7.0a0 - tzdata constrains: - python_abi 3.10.* *_cp310 license: Python-2.0 purls: [] - size: 12232259 - timestamp: 1733407901383 -- conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhff2d567_1.conda - sha256: a50052536f1ef8516ed11a844f9413661829aa083304dc624c5925298d078d79 - md5: 5ba79d7c71f03c678c8ead841f347d6e + size: 13234644 + timestamp: 1772728813900 +- conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + sha256: d6a17ece93bbd5139e02d2bd7dbfa80bee1a4261dced63f65f679121686bf664 + md5: 5b8d21249ff20967101ffa321cab24e8 depends: - python >=3.9 - six >=1.5 + - python license: Apache-2.0 license_family: APACHE purls: - pkg:pypi/python-dateutil?source=hash-mapping - size: 222505 - timestamp: 1733215763718 + size: 233310 + timestamp: 1751104122689 - pypi: https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl name: python-dotenv version: 1.2.2 @@ -4638,30 +4752,32 @@ packages: requires_dist: - click>=5.0 ; extra == 'cli' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/bd/2d/e94b2f7bab6773c70efc70a61d66e312e1febccd9e0db6b9e0adf58cbad1/python_jose-3.3.0-py2.py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/d9/c3/0bd11992072e6a1c513b16500a5d07f91a24017c5909b02c72c62d7ad024/python_jose-3.5.0-py2.py3-none-any.whl name: python-jose - version: 3.3.0 - sha256: 9b1376b023f8b298536eedd47ae1089bcdb848f1535ab30555cd92002d78923a + version: 3.5.0 + sha256: abd1202f23d34dfad2c3d28cb8617b90acf34132c7afd60abd0b0b7d3cb55771 requires_dist: - ecdsa!=0.15 - - rsa - - pyasn1 + - rsa>=4.0,!=4.1.1,!=4.4,<5.0 + - pyasn1>=0.5.0 + - pytest ; extra == 'test' + - pytest-cov ; extra == 'test' - cryptography>=3.4.0 ; extra == 'cryptography' - pycrypto>=2.6.0,<2.7.0 ; extra == 'pycrypto' - - pyasn1 ; extra == 'pycrypto' - pycryptodome>=3.3.1,<4.0.0 ; extra == 'pycryptodome' - - pyasn1 ; extra == 'pycryptodome' -- conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-2.0.7-pyhd8ed1ab_0.conda - sha256: 4790787fe1f4e8da616edca4acf6a4f8ed4e7c6967aa31b920208fc8f95efcca - md5: a61bf9ec79426938ff785eb69dbb1960 + requires_python: '>=3.9' +- conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-3.2.1-pyh332efcf_0.conda + sha256: 1c55116c22512cef7b01d55ae49697707f2c1fd829407183c19817e2d300fd8d + md5: 1cd2f3e885162ee1366312bd1b1677fd depends: - - python >=3.6 + - python >=3.10 + - typing_extensions license: BSD-2-Clause license_family: BSD purls: - pkg:pypi/python-json-logger?source=hash-mapping - size: 13383 - timestamp: 1677079727691 + size: 18969 + timestamp: 1777318679482 - pypi: https://files.pythonhosted.org/packages/41/4a/755a692976425af4627ffba016dd0576a045c70066553a27cc4023d44d58/python-keycloak-0.26.1.tar.gz name: python-keycloak version: 0.26.1 @@ -4669,32 +4785,31 @@ packages: requires_dist: - requests>=2.20.0 - python-jose>=1.4.0 -- conda: https://conda.anaconda.org/conda-forge/noarch/python-kubernetes-32.0.1-pyhd8ed1ab_0.conda - sha256: 6d341f4c4fb5066dac6a8a522e12692cb93ec77132ac2265773d8684f59955e9 - md5: d93b25519ea3ea3fb735060c7bbd9f14 +- conda: https://conda.anaconda.org/conda-forge/noarch/python-kubernetes-35.0.0-pyhd8ed1ab_0.conda + sha256: 5c21f843c2043438d4f7d8a336784c90dea2bc117325dbc83fb6caaf08d5cf32 + md5: c19883b8df056b16f99d1eac8d61eeea depends: - certifi >=14.05.14 - durationpy >=0.7 - google-auth >=1.0.1 - - oauthlib >=3.2.2 - - python >=3.9 + - python >=3.10 - python-dateutil >=2.5.3 - pyyaml >=5.4.1 - requests - requests-oauthlib - six >=1.9.0 - - urllib3 >=1.24.2 + - urllib3 >=1.24.2,!=2.6.0 - websocket-client >=0.32.0,!=0.40.0,!=0.41.*,!=0.42.* license: Apache-2.0 license_family: APACHE purls: - pkg:pypi/kubernetes?source=hash-mapping - size: 478319 - timestamp: 1739952781487 -- pypi: https://files.pythonhosted.org/packages/99/78/4126abcbdbd3c559d43e0db7f7b9173fc6befe45d39a2856cc0b8ec2a5a6/python_multipart-0.0.27-py3-none-any.whl + size: 527564 + timestamp: 1768834003117 +- pypi: https://files.pythonhosted.org/packages/f3/a2/43bbc5860b5034e2af4ef99a0e04d726ff329c43e192ef3abaa8d7ecfce5/python_multipart-0.0.28-py3-none-any.whl name: python-multipart - version: 0.0.27 - sha256: 6fccfad17a27334bd0193681b369f476eda3409f17381a2d65aa7df3f7275645 + version: 0.0.28 + sha256: 10faac07eb966c3f48dc415f9dee46c04cb10d58d30a35677db8027c825ed9b6 requires_python: '>=3.10' - conda: https://conda.anaconda.org/conda-forge/noarch/python-slugify-8.0.4-pyhd8ed1ab_1.conda sha256: a84f270426ae7661f79807b107dedb9829c79bd45f77a3033aa021e10556e87f @@ -4712,28 +4827,28 @@ packages: - pkg:pypi/python-slugify?source=hash-mapping size: 18991 timestamp: 1733756348165 -- conda: https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.10-5_cp310.conda - build_number: 5 - sha256: 074d2f0b31f0333b7e553042b17ea54714b74263f8adda9a68a4bd8c7e219971 - md5: 2921c34715e74b3587b4cff4d36844f9 - constrains: - - python 3.10.* *_cpython - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 6227 - timestamp: 1723823165457 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/python_abi-3.10-5_cp310.conda - build_number: 5 - sha256: 96653ed223e3a8646c22f409936d4c5ac0a22aeef99a3129a86581b80dec484c - md5: c6694ec383fb171da3ab68cae8d0e8f1 +- conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.2-pyhd8ed1ab_0.conda + sha256: e943f9c15a6bdba2e1b9f423ab913b3f6b02197b0ef9f8e6b7464d78b59965b9 + md5: f6ad7450fc21e00ecc23812baed6d2e4 + depends: + - python >=3.10 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/tzdata?source=hash-mapping + size: 146639 + timestamp: 1777068997932 +- conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.10-8_cp310.conda + build_number: 8 + sha256: 7ad76fa396e4bde336872350124c0819032a9e8a0a40590744ff9527b54351c1 + md5: 05e00f3b21e88bb3d658ac700b2ce58c constrains: - python 3.10.* *_cpython license: BSD-3-Clause license_family: BSD purls: [] - size: 6295 - timestamp: 1723823325134 + size: 6999 + timestamp: 1752805924192 - pypi: https://files.pythonhosted.org/packages/ec/dd/96da98f892250475bdf2328112d7468abdd4acc7b902b6af23f4ed958ea0/pytz-2026.2-py2.py3-none-any.whl name: pytz version: '2026.2' @@ -4771,26 +4886,26 @@ packages: - flake8 ; extra == 'tests' - pytest ; extra == 'tests' requires_python: '>=3.8' -- conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.2-py310h89163eb_2.conda - sha256: 5fba7f5babcac872c72f6509c25331bcfac4f8f5031f0102530a41b41336fce6 - md5: fd343408e64cf1e273ab7c710da374db +- conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py310h3406613_1.conda + sha256: f23de6cc72541c6081d3d27482dbc9fc5dd03be93126d9155f06d0cf15d6e90e + md5: 2160894f57a40d2d629a34ee8497795f depends: - __glibc >=2.17,<3.0.a0 - - libgcc >=13 + - libgcc >=14 - python >=3.10,<3.11.0a0 - python_abi 3.10.* *_cp310 - yaml >=0.2.5,<0.3.0a0 license: MIT license_family: MIT purls: - - pkg:pypi/pyyaml?source=compressed-mapping - size: 182769 - timestamp: 1737454971552 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pyyaml-6.0.2-py310heeae437_2.conda - sha256: afc018b83e26056a4d012b00eb97b6af7492edf49cee13b9ad0ddda21aa6551b - md5: b4b5eb2276b14c3a487032bedd1493dc + - pkg:pypi/pyyaml?source=hash-mapping + size: 176522 + timestamp: 1770223379599 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pyyaml-6.0.3-py310h2d8da20_1.conda + sha256: 8acefeb5cc4bcf835f33e45b5cc8b350e738b4954f68c3d8051426e851ca2806 + md5: 7e1a74e779e08e3504230f8216be618b depends: - - libgcc >=13 + - libgcc >=14 - python >=3.10,<3.11.0a0 - python >=3.10,<3.11.0a0 *_cpython - python_abi 3.10.* *_cp310 @@ -4799,8 +4914,8 @@ packages: license_family: MIT purls: - pkg:pypi/pyyaml?source=hash-mapping - size: 174914 - timestamp: 1737454839646 + size: 171618 + timestamp: 1770223419125 - pypi: https://files.pythonhosted.org/packages/30/76/8f099f9d6482450428b17c4d6b241281af7ce6a9de8149ca8c1c649f6792/pyzmq-27.1.0-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl name: pyzmq version: 27.1.0 @@ -4815,34 +4930,35 @@ packages: requires_dist: - cffi ; implementation_name == 'pypy' requires_python: '>=3.8' -- conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda - sha256: 2d6d0c026902561ed77cd646b5021aef2d4db22e57a5b0178dfc669231e06d2c - md5: 283b96675859b20a825f8fa30f311446 +- conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda + sha256: 12ffde5a6f958e285aa22c191ca01bbd3d6e710aa852e00618fa6ddc59149002 + md5: d7d95fc8287ea7bf33e0e7116d2b95ec depends: - - libgcc >=13 + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 - ncurses >=6.5,<7.0a0 license: GPL-3.0-only license_family: GPL purls: [] - size: 282480 - timestamp: 1740379431762 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/readline-8.2-h8382b9d_2.conda - sha256: 54bed3a3041befaa9f5acde4a37b1a02f44705b7796689574bcf9d7beaad2959 - md5: c0f08fc2737967edde1a272d4bf41ed9 + size: 345073 + timestamp: 1765813471974 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/readline-8.3-hb682ff5_0.conda + sha256: fe695f9d215e9a2e3dd0ca7f56435ab4df24f5504b83865e3d295df36e88d216 + md5: 3d49cad61f829f4f0e0611547a9cda12 depends: - - libgcc >=13 + - libgcc >=14 - ncurses >=6.5,<7.0a0 license: GPL-3.0-only license_family: GPL purls: [] - size: 291806 - timestamp: 1740380591358 -- conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.36.2-pyh29332c3_0.conda - sha256: e20909f474a6cece176dfc0dc1addac265deb5fa92ea90e975fbca48085b20c3 - md5: 9140f1c09dd5489549c6a33931b943c7 + size: 357597 + timestamp: 1765815673644 +- conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda + sha256: 0577eedfb347ff94d0f2fa6c052c502989b028216996b45c7f21236f25864414 + md5: 870293df500ca7e18bedefa5838a22ab depends: - attrs >=22.2.0 - - python >=3.9 + - python >=3.10 - rpds-py >=0.7.0 - typing_extensions >=4.4.0 - python @@ -4850,25 +4966,25 @@ packages: license_family: MIT purls: - pkg:pypi/referencing?source=hash-mapping - size: 51668 - timestamp: 1737836872415 -- conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.3-pyhd8ed1ab_1.conda - sha256: d701ca1136197aa121bbbe0e8c18db6b5c94acbd041c2b43c70e5ae104e1d8ad - md5: a9b9368f3701a417eac9edbcae7cb737 + size: 51788 + timestamp: 1760379115194 +- conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.1-pyhcf101f3_0.conda + sha256: 22ffa6f214829b9fb2daac5b25886cca73bd8de1fb9523791ce39478d60a58f7 + md5: 18a1731937516054803c047e1b39dc04 depends: - - certifi >=2017.4.17 + - python >=3.10 + - certifi >=2023.5.7 - charset-normalizer >=2,<4 - idna >=2.5,<4 - - python >=3.9 - - urllib3 >=1.21.1,<3 + - urllib3 >=1.26,<3 + - python constrains: - - chardet >=3.0.2,<6 + - chardet >=3.0.2,<8 license: Apache-2.0 - license_family: APACHE purls: - - pkg:pypi/requests?source=hash-mapping - size: 58723 - timestamp: 1733217126197 + - pkg:pypi/requests?source=compressed-mapping + size: 68706 + timestamp: 1778712882916 - conda: https://conda.anaconda.org/conda-forge/noarch/requests-oauthlib-2.0.0-pyhd8ed1ab_1.conda sha256: 75ef0072ae6691f5ca9709fe6a2570b98177b49d0231a6749ac4e610da934cab md5: a283b764d8b155f81e904675ef5e1f4b @@ -4911,6 +5027,19 @@ packages: - pkg:pypi/rfc3986-validator?source=hash-mapping size: 7818 timestamp: 1598024297745 +- conda: https://conda.anaconda.org/conda-forge/noarch/rfc3987-syntax-1.1.0-pyhe01879c_1.conda + sha256: 70001ac24ee62058557783d9c5a7bbcfd97bd4911ef5440e3f7a576f9e43bc92 + md5: 7234f99325263a5af6d4cd195035e8f2 + depends: + - python >=3.9 + - lark >=1.2.2 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/rfc3987-syntax?source=hash-mapping + size: 22913 + timestamp: 1752876729969 - pypi: https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl name: rich version: 15.0.0 @@ -4920,13 +5049,13 @@ packages: - markdown-it-py>=2.2.0 - pygments>=2.13.0,<3.0.0 requires_python: '>=3.9.0' -- conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-0.23.1-py310hc1293b2_0.conda - sha256: 775f9fe47c18f8c6c4cb706c7837cc04cdc18e6a748fd8964e132d8329975eea - md5: 55afda712d4c48108d993ded1bd4de9b +- conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-0.30.0-py310hd8f68c5_0.conda + sha256: ac1132a9344c77e19bbbdb966668cf73a861ceec7b075858a52c8e961fb8ea9d + md5: 61ff3f8e00c63bb66903636d0197e962 depends: - python + - libgcc >=14 - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - python_abi 3.10.* *_cp310 constrains: - __glibc >=2.17 @@ -4934,14 +5063,14 @@ packages: license_family: MIT purls: - pkg:pypi/rpds-py?source=hash-mapping - size: 391615 - timestamp: 1740153211261 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/rpds-py-0.23.1-py310h2839ab5_0.conda - sha256: dff79045b6da81ebfbd526df925131c68dbdc9b25f0278cd83f9368d3725d9d8 - md5: 71f62673f99ee551cda28a6106785bcb + size: 382893 + timestamp: 1764543243162 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/rpds-py-0.30.0-py310haddc216_0.conda + sha256: 502656baaa06ee999d551f4f300f28f556169b6790066c3af3c20340af6970e6 + md5: eb9b36d85a5cf7f986704c153fd621ba depends: - python - - libgcc >=13 + - libgcc >=14 - python_abi 3.10.* *_cp310 constrains: - __glibc >=2.17 @@ -4949,11 +5078,11 @@ packages: license_family: MIT purls: - pkg:pypi/rpds-py?source=hash-mapping - size: 394927 - timestamp: 1740153507350 -- conda: https://conda.anaconda.org/conda-forge/noarch/rsa-4.9-pyhd8ed1ab_1.conda - sha256: 210ff0e3aaa8ce8e9d45a5fd578ce7b2d5bcd7d3054dc779c3a159b8f72104d6 - md5: 91def14612d11100329d53a75993a4d5 + size: 380660 + timestamp: 1764543343007 +- conda: https://conda.anaconda.org/conda-forge/noarch/rsa-4.9.1-pyhd8ed1ab_0.conda + sha256: e32e94e7693d4bc9305b36b8a4ef61034e0428f58850ebee4675978e3c2e5acf + md5: 58958bb50f986ac0c46f73b6e290d5fe depends: - pyasn1 >=0.1.3 - python >=3.9 @@ -4961,66 +5090,49 @@ packages: license_family: APACHE purls: - pkg:pypi/rsa?source=hash-mapping - size: 30799 - timestamp: 1733662778918 -- conda: https://conda.anaconda.org/conda-forge/linux-64/ruamel.yaml-0.18.10-py310ha75aee5_0.conda - sha256: 5340c3252baf55d99935f95582eb91a4d86a23cf3a205eed09c3a8ac5134517e - md5: 802e3b428e5523507e15ffd8d9ca00b8 + size: 31709 + timestamp: 1744825527634 +- conda: https://conda.anaconda.org/conda-forge/noarch/ruamel.yaml-0.19.1-pyhcf101f3_0.conda + sha256: b48bebe297a63ae60f52e50be328262e880702db4d9b4e86731473ada459c2a1 + md5: 06ad944772941d5dae1e0d09848d8e49 depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - python >=3.10,<3.11.0a0 - - python_abi 3.10.* *_cp310 - - ruamel.yaml.clib >=0.1.2 - license: MIT - license_family: MIT - purls: - - pkg:pypi/ruamel-yaml?source=hash-mapping - size: 203102 - timestamp: 1736248141329 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ruamel.yaml-0.18.10-py310ha766c32_0.conda - sha256: 9c919871b3a470857d8187cc1bf4fc2663d1fa4925dc89542a4a72f43774da43 - md5: c6347cf5e056e8eedc72e6fb377cd9ab - depends: - - libgcc >=13 - - python >=3.10,<3.11.0a0 - - python >=3.10,<3.11.0a0 *_cpython - - python_abi 3.10.* *_cp310 - - ruamel.yaml.clib >=0.1.2 + - python >=3.10 + - ruamel.yaml.clib >=0.2.15 + - python license: MIT license_family: MIT purls: - pkg:pypi/ruamel-yaml?source=hash-mapping - size: 203272 - timestamp: 1736248214500 -- conda: https://conda.anaconda.org/conda-forge/linux-64/ruamel.yaml.clib-0.2.8-py310ha75aee5_1.conda - sha256: 0a1d1dd10f00388e36381e5065f4c94722e225f67f8e4605a07e5b09e6808606 - md5: 5774cc3497be5134eb3a36c4f5c7895b + size: 98448 + timestamp: 1767538149184 +- conda: https://conda.anaconda.org/conda-forge/linux-64/ruamel.yaml.clib-0.2.15-py310h139afa4_1.conda + sha256: 242ff560883541acc447b4fb11f1c6c0a4e91479b70c8ce895aee5d9a8ce346a + md5: a7e3055859e9162d5f7adb9b3c229d56 depends: + - python - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - python >=3.10,<3.11.0a0 + - libgcc >=14 - python_abi 3.10.* *_cp310 license: MIT license_family: MIT purls: - pkg:pypi/ruamel-yaml-clib?source=hash-mapping - size: 146766 - timestamp: 1728724589146 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ruamel.yaml.clib-0.2.8-py310ha766c32_1.conda - sha256: d2f26f362a1751bd600d35f9c840325efabbafe69a77c1208b2f9f3023af6d67 - md5: aeeda4abf3d122d9090f885388cab2dc + size: 152839 + timestamp: 1766159514181 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ruamel.yaml.clib-0.2.15-py310hef25091_1.conda + sha256: fc5cf2db69fab279d0be46a8a077d9354991be5f4d317a73c453fc8df7c69f0a + md5: e91e48a36a6c9f761e259b967878831a depends: - - libgcc >=13 - - python >=3.10,<3.11.0a0 - - python >=3.10,<3.11.0a0 *_cpython + - python + - python 3.10.* *_cpython + - libgcc >=14 - python_abi 3.10.* *_cp310 license: MIT license_family: MIT purls: - pkg:pypi/ruamel-yaml-clib?source=hash-mapping - size: 140150 - timestamp: 1728724732658 + size: 153169 + timestamp: 1766159548278 - pypi: https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl name: secretstorage version: 3.5.0 @@ -5043,33 +5155,34 @@ packages: - pywin32>=305 ; sys_platform == 'win32' and extra == 'nativelib' - pyobjc>=9.0 ; sys_platform == 'darwin' and extra == 'nativelib' requires_python: '>=3.8' -- conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-75.8.2-pyhff2d567_0.conda - sha256: 91d664ace7c22e787775069418daa9f232ee8bafdd0a6a080a5ed2395a6fa6b2 - md5: 9bddfdbf4e061821a1a443f93223be61 +- conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-82.0.1-pyh332efcf_0.conda + sha256: 82088a6e4daa33329a30bc26dc19a98c7c1d3f05c0f73ce9845d4eab4924e9e1 + md5: 8e194e7b992f99a5015edbd4ebd38efd depends: - - python >=3.9 + - python >=3.10 license: MIT license_family: MIT purls: - pkg:pypi/setuptools?source=hash-mapping - size: 777736 - timestamp: 1740654030775 + size: 639697 + timestamp: 1773074868565 - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl name: shellingham version: 1.5.4 sha256: 7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686 requires_python: '>=3.7' -- conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhd8ed1ab_0.conda - sha256: 41db0180680cc67c3fa76544ffd48d6a5679d96f4b71d7498a759e94edc9a2db - md5: a451d576819089b0d672f18768be0f65 +- conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + sha256: 458227f759d5e3fcec5d9b7acce54e10c9e1f4f4b7ec978f3bfd54ce4ee9853d + md5: 3339e3b65d58accf4ca4fb8748ab16b3 depends: - python >=3.9 + - python license: MIT license_family: MIT purls: - pkg:pypi/six?source=hash-mapping - size: 16385 - timestamp: 1733381032766 + size: 18455 + timestamp: 1753199211006 - pypi: https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl name: smmap version: 5.0.3 @@ -5180,70 +5293,82 @@ packages: - pytest ; extra == 'test' - ruff ; extra == 'test' requires_python: '>=3.8' -- conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda - sha256: e0569c9caa68bf476bead1bed3d79650bb080b532c64a4af7d8ca286c08dea4e - md5: d453b98d9c83e71da0741bb0ff4d76bc +- conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda + sha256: cafeec44494f842ffeca27e9c8b0c27ed714f93ac77ddadc6aaf726b5554ebac + md5: cffd3bdd58090148f4cfcd831f4b26ab depends: - - libgcc-ng >=12 - - libzlib >=1.2.13,<2.0.0a0 + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libzlib >=1.3.1,<2.0a0 + constrains: + - xorg-libx11 >=1.8.12,<2.0a0 license: TCL license_family: BSD purls: [] - size: 3318875 - timestamp: 1699202167581 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/tk-8.6.13-h194ca79_0.conda - sha256: 7fa27cc512d3a783f38bd16bbbffc008807372499d5b65d089a8e43bde9db267 - md5: f75105e0585851f818e0009dd1dde4dc + size: 3301196 + timestamp: 1769460227866 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/tk-8.6.13-noxft_h0dc03b3_103.conda + sha256: e25c314b52764219f842b41aea2c98a059f06437392268f09b03561e4f6e5309 + md5: 7fc6affb9b01e567d2ef1d05b84aa6ed depends: - - libgcc-ng >=12 - - libzlib >=1.2.13,<2.0.0a0 + - libgcc >=14 + - libzlib >=1.3.1,<2.0a0 + constrains: + - xorg-libx11 >=1.8.12,<2.0a0 license: TCL license_family: BSD purls: [] - size: 3351802 - timestamp: 1695506242997 -- pypi: https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl - name: tomli - version: 2.4.1 - sha256: 0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe - requires_python: '>=3.8' + size: 3368666 + timestamp: 1769464148928 +- conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda + sha256: 91cafdb64268e43e0e10d30bd1bef5af392e69f00edd34dfaf909f69ab2da6bd + md5: b5325cf06a000c5b14970462ff5e4d58 + depends: + - python >=3.10 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/tomli?source=hash-mapping + size: 21561 + timestamp: 1774492402955 - pypi: https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl name: tomli-w version: 1.2.0 sha256: 188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90 requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/b5/11/87d6d29fb5d237229d67973a6c9e06e048f01cf4994dee194ab0ea841814/tomlkit-0.14.0-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/6a/43/8bd850ee71a191bf072e31302c73a66be413fecdd98fdcd111ecbcce13ca/tomlkit-0.15.0-py3-none-any.whl name: tomlkit - version: 0.14.0 - sha256: 592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680 + version: 0.15.0 + sha256: 4dbc8f0fc024412b57ced8757ac7461305126a648ff8c2c807fcb8e133a78738 requires_python: '>=3.9' -- conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.4.2-py310ha75aee5_0.conda - sha256: 9c2b86d4e58c8b0e7d13a7f4c440f34e2201bae9cfc1d7e1d30a5bc7ffb1d4c8 - md5: 166d59aab40b9c607b4cc21c03924e9d +- conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.5-py310h7c4b9e2_0.conda + sha256: ffe36f9b042eeb8b24f18d769ce7fac76279e8593c9a3bc22f6e43303762b75f + md5: 1a1943b805cbe2538f772a462214d874 depends: - __glibc >=2.17,<3.0.a0 - - libgcc >=13 + - libgcc >=14 - python >=3.10,<3.11.0a0 - python_abi 3.10.* *_cp310 license: Apache-2.0 license_family: Apache purls: - pkg:pypi/tornado?source=hash-mapping - size: 650307 - timestamp: 1732616034421 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/tornado-6.4.2-py310h78583b1_0.conda - sha256: df94626502e7be9c9e269835c8777ec0097fa4536bcde9612486e856b3fd1a53 - md5: 68a2bd5dcbb6feac96dee39f4b49fe0f + size: 668054 + timestamp: 1774358033371 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/tornado-6.5.5-py310ha7967c6_0.conda + sha256: f2e59e5c2030e38fd1244d4e320b55331cd79b9e42f881ea4a44ba648f2a864d + md5: aa470b1224b0e6f14f1dbc508c0f17cc depends: - - libgcc >=13 + - libgcc >=14 - python >=3.10,<3.11.0a0 - python_abi 3.10.* *_cp310 license: Apache-2.0 license_family: Apache purls: - pkg:pypi/tornado?source=hash-mapping - size: 653906 - timestamp: 1732617530355 + size: 667410 + timestamp: 1774359385098 - pypi: https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl name: tqdm version: 4.67.3 @@ -5261,72 +5386,64 @@ packages: - requests ; extra == 'telegram' - ipywidgets>=6 ; extra == 'notebook' requires_python: '>=3.7' -- conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda - sha256: f39a5620c6e8e9e98357507262a7869de2ae8cc07da8b7f84e517c9fd6c2b959 - md5: 019a7385be9af33791c989871317e1ed +- conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.15.0-pyhcf101f3_0.conda + sha256: dfb681579be59c2e790c95f7f49b7529a9b0511d6385ad276e3c8988cbd54d2c + md5: 4bada6a6d908a27262af8ebddf4f7492 depends: - - python >=3.9 + - python >=3.10 + - python license: BSD-3-Clause license_family: BSD purls: - pkg:pypi/traitlets?source=hash-mapping - size: 110051 - timestamp: 1733367480074 + size: 115165 + timestamp: 1778074251714 - pypi: https://files.pythonhosted.org/packages/7b/e3/d81b065a2d866a33a541ac63a2a4cc5737e03ce2379ac3191c98bb8867e3/trove_classifiers-2026.5.7.17-py3-none-any.whl name: trove-classifiers version: 2026.5.7.17 sha256: 5ec0800de5e2ddbd7c663cb4c0c15328f132dc168813897c18866c5c7b93db33 -- conda: https://conda.anaconda.org/conda-forge/noarch/types-python-dateutil-2.9.0.20241206-pyhd8ed1ab_0.conda - sha256: 8b98cd9464837174ab58aaa912fc95d5831879864676650a383994033533b8d1 - md5: 1dbc4a115e2ad9fb7f9d5b68397f66f9 - depends: - - python >=3.9 - license: Apache-2.0 AND MIT - purls: - - pkg:pypi/types-python-dateutil?source=hash-mapping - size: 22104 - timestamp: 1733612458611 -- conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.12.2-hd8ed1ab_1.conda - noarch: python - sha256: c8e9c1c467b5f960b627d7adc1c65fece8e929a3de89967e91ef0f726422fd32 - md5: b6a408c64b78ec7b779a3e5c7a902433 +- conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda + sha256: 7c2df5721c742c2a47b2c8f960e718c930031663ac1174da67c1ed5999f7938c + md5: edd329d7d3a4ab45dcf905899a7a6115 depends: - - typing_extensions 4.12.2 pyha770c72_1 + - typing_extensions ==4.15.0 pyhcf101f3_0 license: PSF-2.0 license_family: PSF purls: [] - size: 10075 - timestamp: 1733188758872 -- pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - name: typing-inspection - version: 0.4.2 - sha256: 4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7 - requires_dist: - - typing-extensions>=4.12.0 - requires_python: '>=3.9' -- conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.12.2-pyha770c72_1.conda - sha256: 337be7af5af8b2817f115b3b68870208b30c31d3439bec07bfb2d8f4823e3568 - md5: d17f13df8b65464ca316cbc000a3cb64 + size: 91383 + timestamp: 1756220668932 +- conda: https://conda.anaconda.org/conda-forge/noarch/typing-inspection-0.4.2-pyhcf101f3_2.conda + sha256: 8b90d2f19f9458b8c58a55e1fcdc1d90c1603a847a47654d8a454549413ba60a + md5: 53f5409c5cfd6c5a66417d68e3f0a864 depends: - - python >=3.9 + - python >=3.10 + - typing_extensions >=4.12.0 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/typing-inspection?source=hash-mapping + size: 20935 + timestamp: 1777105465795 +- conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + sha256: 032271135bca55aeb156cee361c81350c6f3fb203f57d024d7e5a1fc9ef18731 + md5: 0caa1af407ecff61170c9437a808404d + depends: + - python >=3.10 + - python license: PSF-2.0 license_family: PSF purls: - pkg:pypi/typing-extensions?source=hash-mapping - size: 39637 - timestamp: 1733188758212 -- pypi: https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl - name: tzdata - version: '2026.2' - sha256: bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7 - requires_python: '>=2' -- conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025a-h78e105d_0.conda - sha256: c4b1ae8a2931fe9b274c44af29c5475a85b37693999f8c792dad0f8c6734b1de - md5: dbcace4706afdfb7eb891f7b37d07c04 + size: 51692 + timestamp: 1756220668932 +- conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + sha256: 1d30098909076af33a35017eed6f2953af1c769e273a0626a04722ac4acaba3c + md5: ad659d0a2b3e47e38d829aa8cad2d610 license: LicenseRef-Public-Domain purls: [] - size: 122921 - timestamp: 1737119101255 + size: 119135 + timestamp: 1767016325805 - pypi: https://files.pythonhosted.org/packages/61/73/d21edf5b204d1467e06500080a50f79d49ef2b997c79123a536d4a17d97c/uc_micro_py-2.0.0-py3-none-any.whl name: uc-micro-py version: 2.0.0 @@ -5347,19 +5464,21 @@ packages: - pkg:pypi/uri-template?source=hash-mapping size: 23990 timestamp: 1733323714454 -- conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-1.26.19-pyhd8ed1ab_0.conda - sha256: 543ebab5241418a4e0d4d9e356ef13e4361504810a067a01481660bb35eb5643 - md5: 6bb37c314b3cc1515dcf086ffe01c46e +- conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.7.0-pyhd8ed1ab_0.conda + sha256: feff959a816f7988a0893201aa9727bbb7ee1e9cec2c4f0428269b489eb93fb4 + md5: cbb88288f74dbe6ada1c6c7d0a97223e depends: - - brotli-python >=1.0.9 + - backports.zstd >=1.0.0 + - brotli-python >=1.2.0 + - h2 >=4,<5 - pysocks >=1.5.6,<2.0,!=1.5.7 - - python >=3.7 + - python >=3.10 license: MIT license_family: MIT purls: - pkg:pypi/urllib3?source=hash-mapping - size: 115125 - timestamp: 1718728467518 + size: 103560 + timestamp: 1778188657149 - pypi: https://files.pythonhosted.org/packages/43/99/3ec6335ded5b88c2f7ed25c56ffd952546f7ed007ffb1e1539dc3b57015a/userpath-1.9.2-py3-none-any.whl name: userpath version: 1.9.2 @@ -5367,20 +5486,20 @@ packages: requires_dist: - click requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/4c/f8/a5d5bac297b1379719050788c6b852c6b3eefcb1e82d8465ed22c10cede7/uv-0.11.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/12/4d/163fe746b97bd1129627e8b1f943e17583ddc143eaab532d56a799a9ba5a/uv-0.11.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl name: uv - version: 0.11.11 - sha256: d5b9f31dab557b5ee4257d8c6ba2608a63c7278537cb0cd102cf6fc518e3fb5c + version: 0.11.14 + sha256: 379e64b236cf55f762a8308d7efe4365d5296ba29f3a4868761bc45b4e915a71 requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/ee/d5/f3be167a43192062f1409fd6b857a612665d331174293b4ffc73218872e1/uv-0.11.11-py3-none-manylinux_2_28_aarch64.whl +- pypi: https://files.pythonhosted.org/packages/19/fb/7a3673494a0cf70267559166398f9c50c4925ff20122f99a28d6c5a80d83/uv-0.11.14-py3-none-manylinux_2_28_aarch64.whl name: uv - version: 0.11.11 - sha256: 8e8faf2e5b3517155fd18e509b19b21135247d43b7fb9a8d61a44a53118d5ab7 + version: 0.11.14 + sha256: 29c12a562441fc2d604e6920c558cacce74a55f889468708683a79b35a6e18a1 requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/31/a3/5b1562db76a5a488274b2332a97199b32d0442aca0ed193697fd47786316/uvicorn-0.46.0-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/15/41/ac2dfdbc1f60c7af4f994c7a335cfa7040c01642b605d65f611cecc2a1e4/uvicorn-0.47.0-py3-none-any.whl name: uvicorn - version: 0.46.0 - sha256: bbebbcbed972d162afca128605223022bedd345b7bc7855ce66deb31487a9048 + version: 0.47.0 + sha256: 2c5715bc12d1892d84752049f400cd1c3cb018514967fdfeb97640443a6a9432 requires_dist: - click>=7.0 - h11>=0.8 @@ -5393,96 +5512,82 @@ packages: - watchfiles>=0.20 ; extra == 'standard' - websockets>=10.4 ; extra == 'standard' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/ca/ff/ded57ac5ff40a09e6e198550bab075d780941e0b0f83cbeabd087c59383a/virtualenv-20.33.1-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/f6/7f/89d7e8a397752826dbbb81127dfb8feab8286f4f1f91a820f646f04368fb/virtualenv-20.39.1-py3-none-any.whl name: virtualenv - version: 20.33.1 - sha256: 07c19bc66c11acab6a5958b815cbcee30891cd1c2ccf53785a28651a0d8d8a67 + version: 20.39.1 + sha256: a00332e0b089ba64edefbf94ef34e6db33f45fe5184df0652cf0b754dcd36190 requires_dist: - distlib>=0.3.7,<1 - - filelock>=3.12.2,<4 + - filelock>=3.24.2,<4 ; python_full_version >= '3.10' + - filelock>=3.16.1,<=3.19.1 ; python_full_version < '3.10' - importlib-metadata>=6.6 ; python_full_version < '3.8' - platformdirs>=3.9.1,<5 - - furo>=2023.7.26 ; extra == 'docs' - - proselint>=0.13 ; extra == 'docs' - - sphinx>=7.1.2,!=7.3 ; extra == 'docs' - - sphinx-argparse>=0.4 ; extra == 'docs' - - sphinxcontrib-towncrier>=0.2.1a0 ; extra == 'docs' - - towncrier>=23.6 ; extra == 'docs' - - covdefaults>=2.3 ; extra == 'test' - - coverage-enable-subprocess>=1 ; extra == 'test' - - coverage>=7.2.7 ; extra == 'test' - - flaky>=3.7 ; extra == 'test' - - packaging>=23.1 ; extra == 'test' - - pytest-env>=0.8.2 ; extra == 'test' - - pytest-freezer>=0.4.8 ; (python_full_version >= '3.13' and platform_python_implementation == 'CPython' and sys_platform == 'win32' and extra == 'test') or (platform_python_implementation == 'GraalVM' and extra == 'test') or (platform_python_implementation == 'PyPy' and extra == 'test') - - pytest-mock>=3.11.1 ; extra == 'test' - - pytest-randomly>=3.12 ; extra == 'test' - - pytest-timeout>=2.1 ; extra == 'test' - - pytest>=7.4 ; extra == 'test' - - setuptools>=68 ; extra == 'test' - - time-machine>=2.10 ; platform_python_implementation == 'CPython' and extra == 'test' + - typing-extensions>=4.13.2 ; python_full_version < '3.11' requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/41/52/e465037f5375f43533d1a80b6923955201596a99142ed524d77b571a1418/wcwidth-0.7.0-py3-none-any.whl name: wcwidth version: 0.7.0 sha256: 5d69154c429a82910e241c738cd0e2976fac8a2dd47a1a805f4afed1c0f136f2 requires_python: '>=3.8' -- conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-24.11.1-pyhd8ed1ab_0.conda - sha256: 08315dc2e61766a39219b2d82685fc25a56b2817acf84d5b390176080eaacf99 - md5: b49f7b291e15494aafb0a7d74806f337 +- conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-25.10.0-pyhd8ed1ab_0.conda + sha256: 21f6c8a20fe050d09bfda3fb0a9c3493936ce7d6e1b3b5f8b01319ee46d6c6f6 + md5: 6639b6b0d8b5a284f027a2003669aa65 depends: - - python >=3.9 + - python >=3.10 license: BSD-3-Clause license_family: BSD purls: - pkg:pypi/webcolors?source=hash-mapping - size: 18431 - timestamp: 1733359823938 + size: 18987 + timestamp: 1761899393153 - pypi: https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl name: webencodings version: 0.5.1 sha256: a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 -- conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.8.0-pyhd8ed1ab_1.conda - sha256: 1dd84764424ffc82030c19ad70607e6f9e3b9cb8e633970766d697185652053e - md5: 84f8f77f0a9c6ef401ee96611745da8f +- conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda + sha256: 42a2b61e393e61cdf75ced1f5f324a64af25f347d16c60b14117393a98656397 + md5: 2f1ed718fcd829c184a6d4f0f2e07409 depends: - - python >=3.9 + - python >=3.10 license: Apache-2.0 license_family: APACHE purls: - pkg:pypi/websocket-client?source=hash-mapping - size: 46718 - timestamp: 1733157432924 -- conda: https://conda.anaconda.org/conda-forge/linux-64/wget-1.21.4-hda4d442_0.conda - sha256: 70df4ac8cca488618458af4705706551cef7e402bac9c2c41dd17148f60cbd1f - md5: 361e96b664eac64a33c20dfd11affbff + size: 61391 + timestamp: 1759928175142 +- conda: https://conda.anaconda.org/conda-forge/linux-64/wget-1.25.0-h653f8fd_1.conda + sha256: 66a5a05e0e44fd7a57e24d77665d5d6672646a7b316575b2f679108966d9124e + md5: 3b97a15e7345c7f6c51bb57bffad7c93 depends: - - libgcc-ng >=12 + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 - libidn2 >=2,<3.0a0 - libunistring >=0,<1.0a0 - - libzlib >=1.2.13,<2.0.0a0 - - openssl >=3.2.1,<4.0a0 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.5,<4.0a0 + - pcre2 >=10.47,<10.48.0a0 - zlib license: GPL-3.0-or-later license_family: GPL purls: [] - size: 770380 - timestamp: 1710770110704 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/wget-1.21.4-h3861a24_0.conda - sha256: 3515b13665266664d2fd7da6767e83382670dc2bb3a2e91c11ca4ac337b3dac4 - md5: a82f95000b9c2e518f25115f45587d45 + size: 746336 + timestamp: 1772232737502 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/wget-1.25.0-h4f42960_1.conda + sha256: 8fbf0209ec0ec6d1bd4412dc624f5c74c4d3e9b9a4d54995956fa1a6dfb6ad7d + md5: 4e6bb4b78d687478c5f22770aa555162 depends: - - libgcc-ng >=12 + - libgcc >=14 - libidn2 >=2,<3.0a0 - libunistring >=0,<1.0a0 - - libzlib >=1.2.13,<2.0.0a0 - - openssl >=3.2.1,<4.0a0 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.5,<4.0a0 + - pcre2 >=10.47,<10.48.0a0 - zlib license: GPL-3.0-or-later license_family: GPL purls: [] - size: 792543 - timestamp: 1710770044900 + size: 811793 + timestamp: 1772232759430 - pypi: https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl name: widgetsnbextension version: 4.0.15 @@ -5493,33 +5598,34 @@ packages: version: 2026.3.0 sha256: 503183d4b322bfebc3c50cdd21192aa3e81e36c5efbf9133d54ae82143e0576b requires_python: '>=3.8' -- conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h7f98852_2.tar.bz2 - sha256: a4e34c710eeb26945bdbdaba82d3d74f60a78f54a874ec10d373811a5d217535 - md5: 4cb3ad778ec2d5a7acbdf254eb1c42ae +- conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda + sha256: 6d9ea2f731e284e9316d95fa61869fe7bbba33df7929f82693c121022810f4ad + md5: a77f85f77be52ff59391544bfe73390a depends: - - libgcc-ng >=9.4.0 + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 license: MIT license_family: MIT purls: [] - size: 89141 - timestamp: 1641346969816 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/yaml-0.2.5-hf897c2e_2.tar.bz2 - sha256: 8bc601d6dbe249eba44b3c456765265cd8f42ef1e778f8df9b0c9c88b8558d7e - md5: b853307650cb226731f653aa623936a4 + size: 85189 + timestamp: 1753484064210 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/yaml-0.2.5-h80f16a2_3.conda + sha256: 66265e943f32ce02396ad214e27cb35f5b0490b3bd4f064446390f9d67fa5d88 + md5: 032d8030e4a24fe1f72c74423a46fb88 depends: - - libgcc-ng >=9.4.0 + - libgcc >=14 license: MIT license_family: MIT purls: [] - size: 92927 - timestamp: 1641347626613 -- conda: https://conda.anaconda.org/conda-forge/linux-64/yarl-1.18.3-py310h89163eb_1.conda - sha256: 444019585c36b861c556cc995af81106210e036333f9c68b9f9e1c8ea77a1072 - md5: e79f8634942fa1cae3278ca1e529b92c + size: 88088 + timestamp: 1753484092643 +- conda: https://conda.anaconda.org/conda-forge/linux-64/yarl-1.23.0-py310h3406613_0.conda + sha256: aaff325f5f7284a1acf44ccc415dcc9b6964f26401bb17738a8e47d529abea81 + md5: 2d7473b599ae1e2d81c917fe1ec8f2f6 depends: - __glibc >=2.17,<3.0.a0 - idna >=2.0 - - libgcc >=13 + - libgcc >=14 - multidict >=4.0 - propcache >=0.2.1 - python >=3.10,<3.11.0a0 @@ -5528,14 +5634,14 @@ packages: license_family: Apache purls: - pkg:pypi/yarl?source=hash-mapping - size: 140356 - timestamp: 1737576035253 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/yarl-1.18.3-py310heeae437_1.conda - sha256: f2e5cf244f32fe601de4c3849463189d0aabf0bc8bb8d96b3c066fbf896918d7 - md5: 7af9d2503446aeb472bec0c781f68ad2 + size: 135869 + timestamp: 1772409416549 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/yarl-1.23.0-py310h2d8da20_0.conda + sha256: 3d86c7d05eb602e3ea537ea21993d2aba822d316a765b7e362850fbab23a03e0 + md5: d5d37a914072b8c9f6ed846544bb2c96 depends: - idna >=2.0 - - libgcc >=13 + - libgcc >=14 - multidict >=4.0 - propcache >=0.2.1 - python >=3.10,<3.11.0a0 @@ -5545,42 +5651,41 @@ packages: license_family: Apache purls: - pkg:pypi/yarl?source=hash-mapping - size: 138504 - timestamp: 1737576062380 -- conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.21.0-pyhd8ed1ab_1.conda - sha256: 567c04f124525c97a096b65769834b7acb047db24b15a56888a322bf3966c3e1 - md5: 0c3cc595284c5e8f0f9900a9b228a332 + size: 135288 + timestamp: 1772409446668 +- conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.1-pyhcf101f3_0.conda + sha256: 523616c0530d305d2216c2b4a8dfd3872628b60083255b89c5e0d8c42e738cca + md5: e1c36c6121a7c9c76f2f148f1e83b983 depends: - - python >=3.9 + - python >=3.10 + - python license: MIT license_family: MIT purls: - pkg:pypi/zipp?source=hash-mapping - size: 21809 - timestamp: 1732827613585 -- conda: https://conda.anaconda.org/conda-forge/linux-64/zlib-1.3.1-hb9d3cd8_2.conda - sha256: 5d7c0e5f0005f74112a34a7425179f4eb6e73c92f5d109e6af4ddeca407c92ab - md5: c9f075ab2f33b3bbee9e62d4ad0a6cd8 + size: 24461 + timestamp: 1776131454755 +- conda: https://conda.anaconda.org/conda-forge/linux-64/zlib-1.3.2-h25fd6f3_2.conda + sha256: 245c9ee8d688e23661b95e3c6dd7272ca936fabc03d423cdb3cdee1bbcf9f2f2 + md5: c2a01a08fc991620a74b32420e97868a depends: - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - libzlib 1.3.1 hb9d3cd8_2 + - libzlib 1.3.2 h25fd6f3_2 license: Zlib license_family: Other purls: [] - size: 92286 - timestamp: 1727963153079 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/zlib-1.3.1-h86ecc28_2.conda - sha256: b4f649aa3ecdae384d5dad7074e198bff120edd3dfb816588e31738fc6d627b1 - md5: bc230abb5d21b63ff4799b0e75204783 + size: 95931 + timestamp: 1774072620848 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/zlib-1.3.2-hdc9db2a_2.conda + sha256: d651731b45f2d84591881da3ce3e4107a9ba6709fe790dbd5f7b8d9c89a02ed7 + md5: 493587274c81b34d198b085b46a86eaa depends: - - libgcc >=13 - - libzlib 1.3.1 h86ecc28_2 + - libzlib 1.3.2 hdc9db2a_2 license: Zlib license_family: Other purls: [] - size: 95582 - timestamp: 1727963203597 + size: 100515 + timestamp: 1774072641977 - pypi: https://files.pythonhosted.org/packages/73/28/a44bdece01bca027b079f0e00be3b6bd89a4df180071da59a3dd7381665b/zstandard-0.25.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl name: zstandard version: 0.25.0 @@ -5597,28 +5702,24 @@ packages: - cffi~=1.17 ; python_full_version < '3.14' and platform_python_implementation != 'PyPy' and extra == 'cffi' - cffi>=2.0.0b0 ; python_full_version >= '3.14' and platform_python_implementation != 'PyPy' and extra == 'cffi' requires_python: '>=3.9' -- conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_1.conda - sha256: 532d3623961e34c53aba98db2ad0a33b7a52ff90d6960e505fb2d2efc06bb7da - md5: 02e4e2fa41a6528afba2e54cbc4280ff +- conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda + sha256: 68f0206ca6e98fea941e5717cec780ed2873ffabc0e1ed34428c061e2c6268c7 + md5: 4a13eeac0b5c8e5b8ab496e6c4ddd829 depends: - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - libstdcxx >=13 - libzlib >=1.3.1,<2.0a0 license: BSD-3-Clause license_family: BSD purls: [] - size: 567419 - timestamp: 1740255350233 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/zstd-1.5.7-hbcf94c1_1.conda - sha256: a8e9a9e19ec3778594d9746e308cdba096f3019c0c0a62f552d0d299b35c343f - md5: d98196f3502425e14f82bdfc8eb4ae27 + size: 601375 + timestamp: 1764777111296 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/zstd-1.5.7-h85ac4a6_6.conda + sha256: 569990cf12e46f9df540275146da567d9c618c1e9c7a0bc9d9cfefadaed20b75 + md5: c3655f82dcea2aa179b291e7099c1fcc depends: - - libgcc >=13 - - libstdcxx >=13 - libzlib >=1.3.1,<2.0a0 license: BSD-3-Clause license_family: BSD purls: [] - size: 550364 - timestamp: 1740255370714 + size: 614429 + timestamp: 1764777145593 diff --git a/images/jupyterhub/pixi.toml b/images/jupyterhub/pixi.toml index 93ca5bf..ed85853 100644 --- a/images/jupyterhub/pixi.toml +++ b/images/jupyterhub/pixi.toml @@ -19,14 +19,15 @@ python-kubernetes = "*" kubernetes_asyncio = "==29.0.0" jupyterhub-idle-culler = "==1.2.1" sqlalchemy = "==1.4.46" -# jhub-apps#676 requires pyjwt>=2.10. Pin in conda so transitive deps that -# request <2.10 are forced to upgrade rather than pinning to 2.9.0. -pyjwt = ">=2.10,<3" +# jhub-apps 2025.11.1 pins pyjwt<2.10; conda-forge's newer 2.12 would +# violate that. Cap pyjwt here so the conda solve stays compatible. +pyjwt = ">=2.9,<2.10" [pypi-dependencies] nebari-jupyterhub-theme = "==2024.7.1" python-keycloak = "==0.26.1" -# Pinned to nebari-dev/jhub-apps#676 (fall-through-on-non-HS256-Bearer) -# until the fix lands in a tagged release. Required for forwardAccessToken: true -# behind Envoy Gateway. -jhub-apps = { git = "https://github.com/nebari-dev/jhub-apps.git", rev = "5d86277f926136e0ecf253d36b261dd456727855" } +# Tagged release. The earlier git-pin to nebari-dev/jhub-apps#676 +# (Bearer fall-through + Starlette signature fix) is no longer needed: hub +# now owns its own OAuth flow (see chart's 00-gateway-auth.py), so Envoy +# does not inject an RS256 Bearer to /services/japps/* paths. +jhub-apps = "==2025.11.1" From 2f11f263177351dc65baac7d58a3fe6a0653b22e Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Thu, 14 May 2026 22:25:10 +0100 Subject: [PATCH 37/60] chore: bump hub image to sha-ae0969a (GenericOAuthenticator + jhub-apps 2025.11.1) Carries the GenericOAuthenticator switch (config/jupyterhub/00-gateway-auth.py) and jhub-apps 2025.11.1 release from conda-forge. Digest: sha256:e9b481657f34c16b367d402ca1cce79ac64b177dc9eba48f85f35be363958126 --- values.yaml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/values.yaml b/values.yaml index 29fc08c..be4926d 100644 --- a/values.yaml +++ b/values.yaml @@ -207,11 +207,12 @@ jupyterhub: hub: # Custom Nebari hub image with jhub-apps pre-installed. # Tag below is the PR-53 merge-commit SHA at the moment of pinning; the - # corresponding immutable digest is sha256:33d9d7aece1ef63b88a44f3ee9e23685133576dab95d8ffa95a2ddd2a3af6443 - # (carries jhub-apps#676: fall-through bearer + Starlette 1.0 TemplateResponse fix). + # corresponding immutable digest is + # sha256:e9b481657f34c16b367d402ca1cce79ac64b177dc9eba48f85f35be363958126 + # (carries the GenericOAuthenticator switch + jhub-apps 2025.11.1 release). image: name: quay.io/nebari/nebari-data-science-pack-jupyterhub - tag: "sha-ab37dda" + tag: "sha-ae0969a" config: JupyterHub: From 9cb24748a03bd83e3908e5c07542f1464d9ab671 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Thu, 14 May 2026 23:38:43 +0100 Subject: [PATCH 38/60] fix(auth): request openid + profile + email + groups scopes Without explicit scope, GenericOAuthenticator sends no scope= param in the authorize redirect; KC then issues a token that lacks the openid scope, and /userinfo returns 403 at token_to_user. Symptom: 500 on /hub/oauth_callback after the user signs in at KC. Add a unit test that fails if openid drops out of the scope list. --- config/jupyterhub/00-gateway-auth.py | 4 ++++ tests/unit/test_keycloak_authenticator.py | 15 +++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/config/jupyterhub/00-gateway-auth.py b/config/jupyterhub/00-gateway-auth.py index 525a725..31d2e57 100644 --- a/config/jupyterhub/00-gateway-auth.py +++ b/config/jupyterhub/00-gateway-auth.py @@ -58,6 +58,10 @@ def configure( c.KeyCloakOAuthenticator.token_url = urls["token_url"] c.KeyCloakOAuthenticator.userdata_url = urls["userdata_url"] c.KeyCloakOAuthenticator.username_claim = "preferred_username" + # Explicit scopes — GenericOAuthenticator defaults to [] which omits the + # scope param entirely; KC then issues a token without `openid` and + # /userinfo returns 403 at token_to_user. + c.KeyCloakOAuthenticator.scope = ["openid", "profile", "email", "groups"] c.KeyCloakOAuthenticator.claim_groups_key = "groups" c.KeyCloakOAuthenticator.admin_groups = set(admin_groups or ["admin"]) # Persist tokens so refresh_user can use the stored refresh_token. diff --git a/tests/unit/test_keycloak_authenticator.py b/tests/unit/test_keycloak_authenticator.py index 0bb070b..d50d136 100644 --- a/tests/unit/test_keycloak_authenticator.py +++ b/tests/unit/test_keycloak_authenticator.py @@ -114,6 +114,21 @@ def test_logout_url_url_encodes_external_url_safely(): # Cycle 4 — authorization policy (any KC-authenticated user is allowed) # --------------------------------------------------------------------------- +def test_configure_requests_openid_scope_so_userinfo_endpoint_works(): + """Without the openid scope, KC returns 403 at /userinfo. + + GenericOAuthenticator defaults to an empty scope list, which makes + KC omit the openid scope from the issued token; that token can't + call /userinfo, so token_to_user blows up with HTTP 403. + """ + c, _ = _configure_with_defaults() + scopes = c.KeyCloakOAuthenticator.scope + assert "openid" in scopes + # Groups + email round out the claims the spawner / env-list rely on. + assert "groups" in scopes + assert "email" in scopes + + def test_any_keycloak_authenticated_user_is_allowed_by_default(): """The gateway path admitted any KC user; this path must match. From fec173759ebf66a04a989529f2625e2c20ba6210 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Thu, 14 May 2026 23:45:32 +0100 Subject: [PATCH 39/60] chore: bump hub image to sha-ffb035a (openid scope fix) Digest: sha256:4be08f31306c4da35ceccc390688e02947f02d7eab5fbd1efddca90af8bd00fb --- values.yaml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/values.yaml b/values.yaml index be4926d..a5bdc0a 100644 --- a/values.yaml +++ b/values.yaml @@ -208,11 +208,12 @@ jupyterhub: # Custom Nebari hub image with jhub-apps pre-installed. # Tag below is the PR-53 merge-commit SHA at the moment of pinning; the # corresponding immutable digest is - # sha256:e9b481657f34c16b367d402ca1cce79ac64b177dc9eba48f85f35be363958126 - # (carries the GenericOAuthenticator switch + jhub-apps 2025.11.1 release). + # sha256:4be08f31306c4da35ceccc390688e02947f02d7eab5fbd1efddca90af8bd00fb + # (carries the GenericOAuthenticator switch + explicit openid scope + # so KC /userinfo doesn't 403 at token_to_user). image: name: quay.io/nebari/nebari-data-science-pack-jupyterhub - tag: "sha-ae0969a" + tag: "sha-ffb035a" config: JupyterHub: From 76856ca05e642f7926183be6561984f310918369 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Fri, 15 May 2026 00:05:39 +0100 Subject: [PATCH 40/60] =?UTF-8?q?fix(deps):=20cap=20starlette<1=20?= =?UTF-8?q?=E2=80=94=20jhub-apps=202025.11.1=20uses=20legacy=20TemplateRes?= =?UTF-8?q?ponse?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Starlette 1.0 reordered TemplateResponse positional args to (request, name, ...); jhub-apps 2025.11.1 still calls the 2-arg form, which makes /services/japps/create-app 500 at handle_apps. Pin to the last 0.x release until jhub-apps ships a fix. --- images/jupyterhub/pixi.lock | 87 +++++++++++++++++++++---------------- images/jupyterhub/pixi.toml | 5 +++ 2 files changed, 55 insertions(+), 37 deletions(-) diff --git a/images/jupyterhub/pixi.lock b/images/jupyterhub/pixi.lock index fae22a6..06a0879 100644 --- a/images/jupyterhub/pixi.lock +++ b/images/jupyterhub/pixi.lock @@ -15,6 +15,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/aiosignal-1.4.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/alembic-1.18.4-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/annotated-types-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.13.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.4.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/async-timeout-5.0.1-pyhcf101f3_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/async_generator-1.10-pyhd8ed1ab_2.conda @@ -36,6 +37,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/curl-8.20.0-hcf29cc6_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/durationpy-0.10-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/escapism-1.0.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/frozenlist-1.7.0-py310h9548a50_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/git-2.54.0-pl5321h6d3cee1_0.conda @@ -134,6 +136,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-82.0.1-pyh332efcf_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/sqlalchemy-1.4.46-py310h1fa729e_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/starlette-0.52.1-pyhfdc7a7d_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/text-unidecode-1.3-pyhd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda @@ -154,7 +157,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/zlib-1.3.2-h25fd6f3_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda - pypi: https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3b/00/2344469e2084fb287c2e0b57b72910309874c3245463acd6cf5e3db69324/appdirs-1.4.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/09/52/94108adfdd6e2ddf58be64f959a0b9c7d4ef2fa71086c38356d22dc501ea/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl @@ -185,7 +187,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/07/7d/8f7a2eb39f97009001b30a8cdc2223856e56440f7069d7186d06f94e71a5/dulwich-0.24.10-cp310-cp310-manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/51/79/119091c98e2bf49e24ed9f3ae69f816d715d2904aefa6a2baa039a2ba0b0/ecdsa-0.19.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f5/64/82570019b199a85b1d9559e75f448272fc6a79201d8692a1357f3f803837/ensureconda-1.6.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5a/ff/2e4eca3ade2c22fe1dea7043b8ee9dabe47753349eb1b56a202de8af6349/fastapi-0.136.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl @@ -276,7 +277,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a8/45/a132b9074aa18e799b891b91ad72133c98d8042c70f6240e4c5f9dabee2f/structlog-25.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl @@ -301,6 +301,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/aiosignal-1.4.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/alembic-1.18.4-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/annotated-types-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.13.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.4.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/async-timeout-5.0.1-pyhcf101f3_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/async_generator-1.10-pyhd8ed1ab_2.conda @@ -322,6 +323,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/curl-8.20.0-hc57f145_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/durationpy-0.10-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/escapism-1.0.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/frozenlist-1.7.0-py310h6b021dd_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/git-2.54.0-pl5321h5dcfaa0_0.conda @@ -420,6 +422,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-82.0.1-pyh332efcf_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/sqlalchemy-1.4.46-py310h734f5e8_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/starlette-0.52.1-pyhfdc7a7d_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/text-unidecode-1.3-pyhd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/tk-8.6.13-noxft_h0dc03b3_103.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda @@ -440,7 +443,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/zlib-1.3.2-hdc9db2a_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/zstd-1.5.7-h85ac4a6_6.conda - pypi: https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3b/00/2344469e2084fb287c2e0b57b72910309874c3245463acd6cf5e3db69324/appdirs-1.4.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c1/93/44365f3d75053e53893ec6d733e4a5e3147502663554b4d864587c7828a7/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl @@ -471,7 +473,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/9b/71/97f4f643d29ff4737af36560ba669018868a748b1c8e778fc82038e830b8/dulwich-0.24.10-cp310-cp310-manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/51/79/119091c98e2bf49e24ed9f3ae69f816d715d2904aefa6a2baa039a2ba0b0/ecdsa-0.19.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f5/64/82570019b199a85b1d9559e75f448272fc6a79201d8692a1357f3f803837/ensureconda-1.6.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5a/ff/2e4eca3ade2c22fe1dea7043b8ee9dabe47753349eb1b56a202de8af6349/fastapi-0.136.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl @@ -562,7 +563,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a8/45/a132b9074aa18e799b891b91ad72133c98d8042c70f6240e4c5f9dabee2f/structlog-25.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl @@ -709,16 +709,25 @@ packages: - pkg:pypi/annotated-types?source=hash-mapping size: 18074 timestamp: 1733247158254 -- pypi: https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl - name: anyio - version: 4.13.0 - sha256: 08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708 - requires_dist: - - exceptiongroup>=1.0.2 ; python_full_version < '3.11' - - idna>=2.8 - - typing-extensions>=4.5 ; python_full_version < '3.13' - - trio>=0.32.0 ; extra == 'trio' - requires_python: '>=3.10' +- conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.13.0-pyhcf101f3_0.conda + sha256: f09aed24661cd45ba54a43772504f05c0698248734f9ae8cd289d314ac89707e + md5: af2df4b9108808da3dc76710fe50eae2 + depends: + - exceptiongroup >=1.0.2 + - idna >=2.8 + - python >=3.10 + - typing_extensions >=4.5 + - python + constrains: + - trio >=0.32.0 + - uvloop >=0.22.1 + - winloop >=0.2.3 + license: MIT + license_family: MIT + purls: + - pkg:pypi/anyio?source=hash-mapping + size: 146764 + timestamp: 1774359453364 - pypi: https://files.pythonhosted.org/packages/3b/00/2344469e2084fb287c2e0b57b72910309874c3245463acd6cf5e3db69324/appdirs-1.4.4-py2.py3-none-any.whl name: appdirs version: 1.4.4 @@ -1512,14 +1521,17 @@ packages: - pkg:pypi/escapism?source=hash-mapping size: 9444 timestamp: 1734956893639 -- pypi: https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl - name: exceptiongroup - version: 1.3.1 - sha256: a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598 - requires_dist: - - typing-extensions>=4.6.0 ; python_full_version < '3.13' - - pytest>=6 ; extra == 'test' - requires_python: '>=3.7' +- conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + sha256: ee6cf346d017d954255bbcbdb424cddea4d14e4ed7e9813e429db1d795d01144 + md5: 8e662bd460bda79b1ea39194e3c4c9ab + depends: + - python >=3.10 + - typing_extensions >=4.6.0 + license: MIT and PSF-2.0 + purls: + - pkg:pypi/exceptiongroup?source=hash-mapping + size: 21333 + timestamp: 1763918099466 - pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl name: executing version: 2.2.1 @@ -5234,19 +5246,20 @@ packages: - pygments ; extra == 'tests' - littleutils ; extra == 'tests' - cython ; extra == 'tests' -- pypi: https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl - name: starlette - version: 1.0.0 - sha256: d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b - requires_dist: - - anyio>=3.6.2,<5 - - typing-extensions>=4.10.0 ; python_full_version < '3.13' - - httpx>=0.27.0,<0.29.0 ; extra == 'full' - - itsdangerous ; extra == 'full' - - jinja2 ; extra == 'full' - - python-multipart>=0.0.18 ; extra == 'full' - - pyyaml ; extra == 'full' - requires_python: '>=3.10' +- conda: https://conda.anaconda.org/conda-forge/noarch/starlette-0.52.1-pyhfdc7a7d_0.conda + sha256: ab0d09eaee2e35a969e7fca3b5b2fdba35c1f2abb8eb8c66245485155d41868e + md5: 7ee23ae71c6c1e2f2fe9ea7cf00f1a8e + depends: + - anyio >=3.6.2,<5 + - python >=3.10 + - typing_extensions >=4.10.0 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/starlette?source=hash-mapping + size: 64896 + timestamp: 1768919444896 - pypi: https://files.pythonhosted.org/packages/a8/45/a132b9074aa18e799b891b91ad72133c98d8042c70f6240e4c5f9dabee2f/structlog-25.5.0-py3-none-any.whl name: structlog version: 25.5.0 diff --git a/images/jupyterhub/pixi.toml b/images/jupyterhub/pixi.toml index ed85853..94a110f 100644 --- a/images/jupyterhub/pixi.toml +++ b/images/jupyterhub/pixi.toml @@ -22,6 +22,11 @@ sqlalchemy = "==1.4.46" # jhub-apps 2025.11.1 pins pyjwt<2.10; conda-forge's newer 2.12 would # violate that. Cap pyjwt here so the conda solve stays compatible. pyjwt = ">=2.9,<2.10" +# Starlette 1.0 changed TemplateResponse() positional arg order +# (request, name, ...) — jhub-apps 2025.11.1 still calls the legacy +# 2-arg form, blowing up /services/japps/create-app with 500. Cap until +# jhub-apps catches up. +starlette = ">=0.36,<1" [pypi-dependencies] nebari-jupyterhub-theme = "==2024.7.1" From 03234f1058adac2ed78ecf8d99e85d0f480d43c6 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Fri, 15 May 2026 00:10:31 +0100 Subject: [PATCH 41/60] chore: bump hub image to sha-8676046 (starlette<1 cap) Digest: sha256:8e007b6dc55ffe5f451d016610f0733462de323df5bb2af3235a6f17b22e5ddf --- values.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/values.yaml b/values.yaml index a5bdc0a..fc6a0a8 100644 --- a/values.yaml +++ b/values.yaml @@ -208,12 +208,12 @@ jupyterhub: # Custom Nebari hub image with jhub-apps pre-installed. # Tag below is the PR-53 merge-commit SHA at the moment of pinning; the # corresponding immutable digest is - # sha256:4be08f31306c4da35ceccc390688e02947f02d7eab5fbd1efddca90af8bd00fb - # (carries the GenericOAuthenticator switch + explicit openid scope - # so KC /userinfo doesn't 403 at token_to_user). + # sha256:8e007b6dc55ffe5f451d016610f0733462de323df5bb2af3235a6f17b22e5ddf + # (carries the GenericOAuthenticator switch, openid scope, and + # starlette<1 cap so /services/japps/create-app doesn't 500). image: name: quay.io/nebari/nebari-data-science-pack-jupyterhub - tag: "sha-ffb035a" + tag: "sha-8676046" config: JupyterHub: From 8ceaf0d8781498e8d0a42b68edf4cfd6334439e8 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Fri, 15 May 2026 00:36:21 +0100 Subject: [PATCH 42/60] docs: mark HANDOFF-stale-token.md resolved (GenericOAuthenticator switch) Tested end-to-end on hetzner via Playwright headless: - /hub/oauth_callback returns 302 to /hub/home (no 500) - /services/japps/create-app renders (starlette<1 cap) - /services/japps/conda-environments/ returns 200 - After 6-min idle, refresh_token grant fires, token stays fresh - 3-step KC -> Nebi token exchange succeeds end-to-end --- HANDOFF-stale-token.md | 138 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 HANDOFF-stale-token.md diff --git a/HANDOFF-stale-token.md b/HANDOFF-stale-token.md new file mode 100644 index 0000000..3895ba2 --- /dev/null +++ b/HANDOFF-stale-token.md @@ -0,0 +1,138 @@ +# RESOLVED 2026-05-15 — switched hub to GenericOAuthenticator + +The env-list stale-token symptom is gone in production. Hub now runs +JupyterHub's `KeyCloakOAuthenticator(GenericOAuthenticator)` instead of +reading Envoy-set cookies; the built-in `refresh_user` runs the +refresh_token grant on `auth_refresh_age` (240s) so the stored +access_token never goes stale. + +**Proof from hub log on hetzner**: + +``` +23:28:27 token-exchange step 1: refresh succeeded — valid for 300s +23:34:29 token-exchange step 1: refresh succeeded — valid for 300s (6 min later) +nebi-envs: listed 0 ready environments (test user has no workspaces) +``` + +Pre-fix the 23:34 probe would have returned `EXPIRED` and `400 +invalid_request` from KC at step 2. + +**Landed PRs**: +- nebari-data-science-pack #53 — `KeyCloakOAuthenticator`, chart wiring, + `enforceAtGateway: false` for hub, starlette<1 cap, jhub-apps 2025.11.1 + (off the git pin), unit tests. +- hetzner-nebari main (PR #1) — Secret mount, OAUTH_* env vars, + service_workers: 1 to fit under hub's 10s managed-service timeout, + JUPYTERHUB_OIDC_CLIENT_SECRET re-exposed as env for 03-nebi-envs.py. + +The old design notes below are kept for the archive; they describe the +symptom + the dead-ends we ruled out (cache nebi JWT, plumb Bearer +through jhub-apps) before landing the GenericOAuthenticator switch. + +--- + +# Stale access-token in jhub-apps env selector — handoff + +## Symptom + +`/services/japps/conda-environments/` returns `[]` ~5 min after a fresh Keycloak login, so the **Software Environment** dropdown on the *Create App* page disappears. Refreshing the page does not help. Hard-clearing cookies + logging in again restores it for another ~5 min. + +## What works today + +End-to-end chain is wired correctly: + +| Layer | Confirmed | +|-------|-----------| +| Keycloak v26.5 standard token exchange (V2) on `jupyterhub-…` client | `standard.token.exchange.enabled=true` (operator) | +| Audience mapper for `nebi-…` on hub client | `oidc-audience-mapper` (operator) | +| Hub client allowed to exchange to nebi audience | yes | +| Nebi accepts the resulting JWT | yes | +| ds-pack chart passthrough for `enforceAtGateway` / `forwardAccessToken` | yes | +| `_extract_envoy_cookies` reads `Authorization: Bearer` first, cookie fallback | yes (PR #53) | +| Stale-token re-fetch in `03-nebi-envs.py` | yes (PR #53) | + +## Why it still breaks at ~5 min + +Hub stores per-user `auth_state` (id_token + access_token) in its DB at OAuth callback time. After that: + +- Envoy Gateway v1.6 **does not rotate the `AccessToken-*` cookie content** even with `oidc.refreshToken: true` set on the SecurityPolicy. Envoy refreshes the access token internally (server-side) but only updates cookies in some narrow paths — not on every upstream request. +- Hub's `EnvoyOIDCAuthenticator.refresh_user` only fires when JupyterHub itself receives a request **with a `handler` AND with the IdToken cookie**. jhub-apps service requests to `/services/japps/*` go to japps, not hub, so they never trigger `refresh_user`. Hub's `auth_state` stays at whatever was captured at last `/hub/*` browser hit. +- The env-listing callable (`get_nebi_environments`) reads `user["auth_state"]` via the hub API, which returns the stale stored value — there is no Hub API endpoint that "refresh and return". The recent `_fetch_fresh_auth_state` mitigation in `03-nebi-envs.py` is a no-op because the hub's stored value is itself stale. +- Default Keycloak access-token lifespan is **5 min**. After that, Keycloak rejects the token at step 2 of token exchange with `400 invalid_request "Invalid token"` and the callable returns `[]`. + +## What we tried and ruled out + +### `forwardAccessToken: true` on the SecurityPolicy +Makes Envoy inject `Authorization: Bearer ` on every upstream request, fixing the freshness problem. **But** jhub-apps's `service.security.get_current_user` reads the `Authorization` header **before** its own cookie: + +```python +auth_header = OAuth2AuthorizationCodeBearer(...) # <-- reads Authorization +... +token = auth_param or auth_header or auth_cookie +token = _get_jhub_token_from_jwt_token(token) # tries HS256 decode +``` + +The Keycloak access token is RS256, so `jwt.decode(..., algorithms=["HS256"])` throws `InvalidAlgorithmError`, jhub-apps returns 401, the browser is redirected to `/jhub-login`, OAuth round-trips, sets a new cookie, and the next request hits the same path — **infinite loop in the UI**. + +`forwardAccessToken` is therefore defaulted to `false` in `values.yaml` until jhub-apps is patched. + +### Cache `workspace_list` for 10 min (`87cd6c7`, reverted in `ec6f135`) +Hides the symptom for 10 min, but the next request after the cache expires still requires a fresh access token, which is no longer fresh — same failure mode at a longer interval. + +### Cache the **Nebi JWT** for 24 h +The third leg of the exchange returns a Nebi JWT with a 24 h lifetime. Caching that per user means token exchange runs **once per 24 h per user** — virtually always within the post-login window where the access token is still fresh. Workspace list is re-fetched from Nebi on every page load using the cached Bearer. + +This is the recommended fix and has not been merged yet. Skeleton: + +```python +# 03-nebi-envs.py +import time, threading, json, base64 + +_jwt_cache = {} # username -> (nebi_jwt, exp_unix) +_jwt_cache_lock = threading.Lock() + +def _cached_nebi_jwt(username): + with _jwt_cache_lock: + entry = _jwt_cache.get(username) + if entry and entry[1] > time.time() + 30: + return entry[0] + return None + +def _store_nebi_jwt(username, jwt_str): + payload = jwt_str.split(".")[1] + "=" * (4 - len(jwt_str.split(".")[1]) % 4) + exp = json.loads(base64.urlsafe_b64decode(payload)).get("exp", 0) + with _jwt_cache_lock: + _jwt_cache[username] = (jwt_str, exp) + +def get_nebi_environments(user): + username = user.get("name", "") + nebi_jwt = _cached_nebi_jwt(username) + if not nebi_jwt: + # cold path — needs fresh access token + nebi_jwt = _do_token_exchange(...) # existing logic + if nebi_jwt: + _store_nebi_jwt(username, nebi_jwt) + if not nebi_jwt: + return [] + return _list_workspaces_with_jwt(nebi_jwt, username) +``` + +Caveats: +- Nebi role / group changes for the user only take effect on next exchange (next login or 24 h). Acceptable. +- Cache lives in the japps **service** process (one of `service_workers` uvicorn workers). Each worker has its own dict — first miss in any worker triggers an exchange. Optional: shared cache via Redis or file. Not urgent. +- Cold-start path still fails if the very first env-listing call after hub restart happens >5 min after the user's last `/hub/*` hit. Mitigation: the spawner already runs the same exchange at spawn time. Hub could write the resulting JWT to a per-user file (`/tmp/nebi-jwt-`) and the env-listing callable could read that as the warm-cache source. Optional follow-up. + +## Other live work + +- **operator PR #116** — V2 attribute + audience mapper, merged-ready. The build-multiarch workflow has a temp `needs: []` stub that must be reverted before review. +- **ds-pack PR #53** — stale-token re-fetch + chart passthrough + Bearer-header reader + `forwardAccessToken: false` default. +- **hetzner-nebari main** — already pointing at the operator fix branch and the ds-pack PR branch. Revert to stable tags once both PRs land in releases. +- **nebari.openteams.ai** — local commit `15790f8` bumps the operator kustomize ref to the fix branch. Not pushed. + +## Files of interest + +- `config/jupyterhub/01-spawner.py` — `get_nebi_jwt` helper (3-step exchange), `_fetch_fresh_auth_state`, `_nebi_pre_spawn_hook`. +- `config/jupyterhub/03-nebi-envs.py` — current `get_nebi_environments` callable. Add the JWT cache here. +- `config/jupyterhub/00-gateway-auth.py` — `EnvoyOIDCAuthenticator.refresh_user`. Already reads `Authorization: Bearer` first when present. +- `templates/nebariapp.yaml` — chart passes `enforceAtGateway`, `forwardAccessToken`, `tokenExchange` through to the operator. +- `values.yaml` — `nebariapp.auth.enforceAtGateway: true`, `forwardAccessToken: false` (default). From c2443b00c207e8f55977946762c41fe3f2012207 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Fri, 15 May 2026 10:33:04 +0100 Subject: [PATCH 43/60] feat(auth): auto-login + KC end-session with id_token_hint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two UX fixes: 1. auto_login=True Hub now 302s /hub/login directly to KC instead of rendering the local form with a 'Sign in with OAuth 2.0' link. Single IdP — no point making the user click through. 2. KeyCloakLogoutHandler KC v18+ rejects /protocol/openid-connect/logout when post_logout_redirect_uri is present without id_token_hint. The static logout_redirect_url can't include it (per-user), so install a handler that reads auth_state.id_token at request time and builds the URL. Falls back to no-hint URL if auth_state is missing (legacy session). --- config/jupyterhub/00-gateway-auth.py | 75 +++++++++++++++++++++-- tests/unit/test_keycloak_authenticator.py | 48 +++++++++++++++ 2 files changed, 119 insertions(+), 4 deletions(-) diff --git a/config/jupyterhub/00-gateway-auth.py b/config/jupyterhub/00-gateway-auth.py index 31d2e57..0cda65f 100644 --- a/config/jupyterhub/00-gateway-auth.py +++ b/config/jupyterhub/00-gateway-auth.py @@ -18,14 +18,72 @@ import os from pathlib import Path -from urllib.parse import quote +from urllib.parse import quote, urlencode +from jupyterhub.handlers.login import LogoutHandler from oauthenticator.generic import GenericOAuthenticator +def _build_logout_url( + *, + end_session_url: str, + id_token: str | None, + post_logout_redirect_uri: str, +) -> str: + """Build a KC end-session URL with id_token_hint + post_logout_redirect_uri. + + Keycloak v18+ rejects logout without ``id_token_hint`` when a + ``post_logout_redirect_uri`` is also given. ``id_token`` may be None + if the user's auth_state was never populated (legacy session); fall + back to just the redirect. + """ + params = {"post_logout_redirect_uri": post_logout_redirect_uri} + if id_token: + params["id_token_hint"] = id_token + return f"{end_session_url}?{urlencode(params)}" + + +class KeyCloakLogoutHandler(LogoutHandler): + """Bounce hub logout through Keycloak's end_session endpoint. + + Hub's local logout only clears its own cookies; KC keeps the user's + session alive, so the next /hub/login transparently re-authenticates. + Pass the user's id_token as ``id_token_hint`` so KC actually + terminates the upstream session. + """ + + async def render_logout_page(self): + user = self.current_user + id_token = None + if user is not None: + try: + auth_state = await user.get_auth_state() + if auth_state: + id_token = auth_state.get("id_token") + except Exception: + self.log.warning( + "logout: failed reading auth_state for %s — proceeding without id_token_hint", + user.name, exc_info=True, + ) + url = _build_logout_url( + end_session_url=self.authenticator._kc_end_session_url, + id_token=id_token, + post_logout_redirect_uri=self.authenticator._kc_post_logout_redirect_uri, + ) + self.redirect(url) + + class KeyCloakOAuthenticator(GenericOAuthenticator): """Marker subclass so traitlets config can target it explicitly.""" + # Stashed by configure() so the logout handler can build URLs at request time. + _kc_end_session_url: str = "" + _kc_post_logout_redirect_uri: str = "" + + def get_handlers(self, app): + # Append KeyCloakLogoutHandler to whatever oauthenticator provides. + return list(super().get_handlers(app)) + [("/logout", KeyCloakLogoutHandler)] + def _kc_urls(issuer: str) -> dict: """Derive Keycloak OIDC endpoint URLs from the realm issuer URL.""" @@ -69,13 +127,22 @@ def configure( c.KeyCloakOAuthenticator.refresh_pre_spawn = True # Refresh ~1 min before KC's 5-min access-token TTL expires. c.KeyCloakOAuthenticator.auth_refresh_age = 240 - # Hub logout must terminate the upstream Keycloak session, otherwise - # the next /hub/ request silently re-uses it. Bounce through KC's - # end_session_endpoint with post_logout_redirect_uri pointing back here. + # Hub logout must terminate the upstream Keycloak session. KC v18+ + # requires id_token_hint when post_logout_redirect_uri is given — + # that's per-user, so KeyCloakLogoutHandler builds the URL at request + # time. logout_redirect_url remains set as a fallback for any code + # path that doesn't go through the handler. c.KeyCloakOAuthenticator.logout_redirect_url = ( f"{urls['end_session_url']}" f"?post_logout_redirect_uri={quote(external_url, safe='')}" ) + # Stash the pieces the logout handler needs at request time. + c.KeyCloakOAuthenticator._kc_end_session_url = urls["end_session_url"] + c.KeyCloakOAuthenticator._kc_post_logout_redirect_uri = external_url + # Skip hub's local /hub/login form — go straight to Keycloak's + # authorize endpoint. One IdP, no point making the user click a + # "Sign in with OAuth 2.0" button. + c.Authenticator.auto_login = True # Any KC-authenticated user is admitted (matches prior EnvoyOIDC policy); # tighten via admin_groups / allowed_groups per-deploy if needed. c.Authenticator.allow_all = True diff --git a/tests/unit/test_keycloak_authenticator.py b/tests/unit/test_keycloak_authenticator.py index d50d136..2bfd532 100644 --- a/tests/unit/test_keycloak_authenticator.py +++ b/tests/unit/test_keycloak_authenticator.py @@ -129,6 +129,54 @@ def test_configure_requests_openid_scope_so_userinfo_endpoint_works(): assert "email" in scopes +def test_configure_enables_auto_login_so_hub_skips_local_login_form(): + """auto_login=True makes /hub/login 302 to the IdP, not render a form. + + Without this, users see hub's "Sign in with OAuth 2.0" page with a + button to click — pointless friction when there's one IdP. + """ + c, _ = _configure_with_defaults() + assert c.Authenticator.auto_login is True + + +def test_keycloak_authenticator_serves_custom_logout_handler(): + """KC v18+ requires id_token_hint at end_session_endpoint. + + A static logout_redirect_url can't include it (per-user), so the + authenticator must install a handler that builds the URL per request. + """ + c, mod = _configure_with_defaults() + handlers = mod.KeyCloakOAuthenticator().get_handlers(app=None) + paths = [h[0] for h in handlers] + assert "/logout" in paths + + +def test_build_logout_url_includes_id_token_hint_and_post_redirect(): + c, mod = _configure_with_defaults() + url = mod._build_logout_url( + end_session_url=f"{ISSUER}/protocol/openid-connect/logout", + id_token="header.payload.signature", + post_logout_redirect_uri=EXTERNAL, + ) + assert url.startswith(f"{ISSUER}/protocol/openid-connect/logout?") + assert "id_token_hint=header.payload.signature" in url + assert "post_logout_redirect_uri=" in url + assert "https%3A%2F%2Fhub.example.test" in url + + +def test_build_logout_url_omits_id_token_hint_when_missing(): + """Token may be absent (legacy session): still produce a usable URL + that at least clears local cookies and bounces back.""" + c, mod = _configure_with_defaults() + url = mod._build_logout_url( + end_session_url=f"{ISSUER}/protocol/openid-connect/logout", + id_token=None, + post_logout_redirect_uri=EXTERNAL, + ) + assert "id_token_hint=" not in url + assert "post_logout_redirect_uri=" in url + + def test_any_keycloak_authenticated_user_is_allowed_by_default(): """The gateway path admitted any KC user; this path must match. From e2d14ed679cec9650d134bdef033498615a64a82 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Fri, 15 May 2026 10:35:47 +0100 Subject: [PATCH 44/60] chore: bump hub image to sha-0e393cb (auto_login + KC end-session) Digest: sha256:93f7139b8775b7a22ac4db313583c83cded449ac77302229e1060f27bce3d6c1 --- values.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/values.yaml b/values.yaml index fc6a0a8..0f5a95c 100644 --- a/values.yaml +++ b/values.yaml @@ -208,12 +208,12 @@ jupyterhub: # Custom Nebari hub image with jhub-apps pre-installed. # Tag below is the PR-53 merge-commit SHA at the moment of pinning; the # corresponding immutable digest is - # sha256:8e007b6dc55ffe5f451d016610f0733462de323df5bb2af3235a6f17b22e5ddf - # (carries the GenericOAuthenticator switch, openid scope, and - # starlette<1 cap so /services/japps/create-app doesn't 500). + # sha256:93f7139b8775b7a22ac4db313583c83cded449ac77302229e1060f27bce3d6c1 + # (carries the GenericOAuthenticator switch, openid scope, starlette<1 + # cap, plus auto_login=True and KC end-session id_token_hint handler). image: name: quay.io/nebari/nebari-data-science-pack-jupyterhub - tag: "sha-8676046" + tag: "sha-0e393cb" config: JupyterHub: From c987087972a4fda0c1ef42d088b08b6c3b530334 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Fri, 15 May 2026 10:39:51 +0100 Subject: [PATCH 45/60] fix(auth): override LogoutHandler.get to inject id_token_hint Base LogoutHandler.get() short-circuits to authenticator.logout_redirect_url when auto_login=True, so the prior override of render_logout_page never fired. Move the per-user URL building into get() itself, with default_handle_logout + handle_logout still called so hub's local session state is cleared. --- config/jupyterhub/00-gateway-auth.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/config/jupyterhub/00-gateway-auth.py b/config/jupyterhub/00-gateway-auth.py index 0cda65f..096dc35 100644 --- a/config/jupyterhub/00-gateway-auth.py +++ b/config/jupyterhub/00-gateway-auth.py @@ -50,9 +50,13 @@ class KeyCloakLogoutHandler(LogoutHandler): session alive, so the next /hub/login transparently re-authenticates. Pass the user's id_token as ``id_token_hint`` so KC actually terminates the upstream session. + + We override ``get()`` (not ``render_logout_page``) because the base + ``LogoutHandler.get`` short-circuits to ``authenticator.logout_redirect_url`` + when ``auto_login=True`` and never invokes ``render_logout_page``. """ - async def render_logout_page(self): + async def get(self): user = self.current_user id_token = None if user is not None: @@ -65,6 +69,10 @@ async def render_logout_page(self): "logout: failed reading auth_state for %s — proceeding without id_token_hint", user.name, exc_info=True, ) + # Clear hub's local session cookies; LogoutHandler.default_handle_logout + # handles login_user-revocation + stop-server. + await self.default_handle_logout() + await self.handle_logout() url = _build_logout_url( end_session_url=self.authenticator._kc_end_session_url, id_token=id_token, From 7182292bddb825ea0abfbf6239f89f57df684d52 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Fri, 15 May 2026 10:42:48 +0100 Subject: [PATCH 46/60] chore: bump hub image to sha-38305c6 (logout id_token_hint fix) --- values.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/values.yaml b/values.yaml index 0f5a95c..f8db74c 100644 --- a/values.yaml +++ b/values.yaml @@ -208,12 +208,12 @@ jupyterhub: # Custom Nebari hub image with jhub-apps pre-installed. # Tag below is the PR-53 merge-commit SHA at the moment of pinning; the # corresponding immutable digest is - # sha256:93f7139b8775b7a22ac4db313583c83cded449ac77302229e1060f27bce3d6c1 - # (carries the GenericOAuthenticator switch, openid scope, starlette<1 - # cap, plus auto_login=True and KC end-session id_token_hint handler). + # sha256:1934b220d22b7e1a1d920fd2aef97bdf9a5925ec153b721f5f4e37fc936e16f1 + # (GenericOAuthenticator + openid scope + starlette<1 + auto_login=True + # + KC end-session id_token_hint via LogoutHandler.get override). image: name: quay.io/nebari/nebari-data-science-pack-jupyterhub - tag: "sha-0e393cb" + tag: "sha-38305c6" config: JupyterHub: From 8b395d5137b3a451be36b3377b0c128bc2d548ae Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Fri, 15 May 2026 10:46:18 +0100 Subject: [PATCH 47/60] fix(auth): monkey-patch LogoutHandler.get so /hub/logout uses our handler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Authenticator-supplied handlers are appended after jupyterhub's defaults in init_handlers, so tornado's first-match routing picks the default LogoutHandler at /logout — our override via get_handlers is a dead route. Monkey-patch the base LogoutHandler.get instead. --- config/jupyterhub/00-gateway-auth.py | 39 +++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/config/jupyterhub/00-gateway-auth.py b/config/jupyterhub/00-gateway-auth.py index 096dc35..2c9ca31 100644 --- a/config/jupyterhub/00-gateway-auth.py +++ b/config/jupyterhub/00-gateway-auth.py @@ -20,6 +20,7 @@ from pathlib import Path from urllib.parse import quote, urlencode +import jupyterhub.handlers.login as _hub_login_handlers from jupyterhub.handlers.login import LogoutHandler from oauthenticator.generic import GenericOAuthenticator @@ -88,9 +89,39 @@ class KeyCloakOAuthenticator(GenericOAuthenticator): _kc_end_session_url: str = "" _kc_post_logout_redirect_uri: str = "" - def get_handlers(self, app): - # Append KeyCloakLogoutHandler to whatever oauthenticator provides. - return list(super().get_handlers(app)) + [("/logout", KeyCloakLogoutHandler)] + +def _install_logout_patch(): + """Replace jupyterhub.handlers.login.LogoutHandler.get with ours. + + The Authenticator's get_handlers list is appended AFTER jupyterhub's + default handlers in init_handlers, so tornado's first-match routing + picks the default LogoutHandler at /logout — our overrides via + get_handlers are dead routes. Monkey-patching the base class is the + least-surprising way to inject id_token_hint without forking the + handler registry. + """ + async def get(self): + user = self.current_user + id_token = None + if user is not None: + try: + auth_state = await user.get_auth_state() + if auth_state: + id_token = auth_state.get("id_token") + except Exception: + self.log.warning( + "logout: failed reading auth_state for %s — proceeding " + "without id_token_hint", user.name, exc_info=True, + ) + await self.default_handle_logout() + await self.handle_logout() + url = _build_logout_url( + end_session_url=self.authenticator._kc_end_session_url, + id_token=id_token, + post_logout_redirect_uri=self.authenticator._kc_post_logout_redirect_uri, + ) + self.redirect(url) + _hub_login_handlers.LogoutHandler.get = get def _kc_urls(issuer: str) -> dict: @@ -147,6 +178,8 @@ def configure( # Stash the pieces the logout handler needs at request time. c.KeyCloakOAuthenticator._kc_end_session_url = urls["end_session_url"] c.KeyCloakOAuthenticator._kc_post_logout_redirect_uri = external_url + # Patch the base LogoutHandler so /hub/logout terminates the KC session. + _install_logout_patch() # Skip hub's local /hub/login form — go straight to Keycloak's # authorize endpoint. One IdP, no point making the user click a # "Sign in with OAuth 2.0" button. From f3788a462b348d544c885a40537531a7fe5449a2 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Fri, 15 May 2026 10:49:04 +0100 Subject: [PATCH 48/60] chore: bump hub image to sha-8ff8d0a (logout monkey-patch) --- values.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/values.yaml b/values.yaml index f8db74c..02ff5a6 100644 --- a/values.yaml +++ b/values.yaml @@ -208,12 +208,12 @@ jupyterhub: # Custom Nebari hub image with jhub-apps pre-installed. # Tag below is the PR-53 merge-commit SHA at the moment of pinning; the # corresponding immutable digest is - # sha256:1934b220d22b7e1a1d920fd2aef97bdf9a5925ec153b721f5f4e37fc936e16f1 + # sha256:2fdea3efbd07fe8fe426992bb54ee307490d06b521243c32f7f5e95719271e18 # (GenericOAuthenticator + openid scope + starlette<1 + auto_login=True - # + KC end-session id_token_hint via LogoutHandler.get override). + # + LogoutHandler monkey-patch so /hub/logout sends id_token_hint to KC). image: name: quay.io/nebari/nebari-data-science-pack-jupyterhub - tag: "sha-38305c6" + tag: "sha-8ff8d0a" config: JupyterHub: From 4cff508a4b5569177a4670dec187650ab67780fe Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Fri, 15 May 2026 10:56:15 +0100 Subject: [PATCH 49/60] fix(auth): use OAuthenticator.logout_handler hook (no monkey-patch) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OAuthenticator.get_handlers reads the class-level logout_handler attribute when registering the /logout route. Swap it to our subclass (class attr on KeyCloakOAuthenticator) instead of monkey-patching LogoutHandler.get or duplicating the /logout entry — the latter just appends a second tuple after oauthenticator's own (r'/logout', OAuthLogoutHandler), and tornado's first-match keeps picking the base class. KeyCloakLogoutHandler subclasses OAuthLogoutHandler and overrides render_logout_page (not get) so the inherited LogoutHandler.get still runs default_handle_logout + handle_logout (token revocation, cookie clear). For that to happen, authenticator.logout_redirect_url is left empty — otherwise LogoutHandler.get short-circuits when auto_login is True and never calls render_logout_page. --- config/jupyterhub/00-gateway-auth.py | 87 +++++++---------------- tests/unit/test_keycloak_authenticator.py | 45 +++++------- 2 files changed, 43 insertions(+), 89 deletions(-) diff --git a/config/jupyterhub/00-gateway-auth.py b/config/jupyterhub/00-gateway-auth.py index 2c9ca31..0cbc70e 100644 --- a/config/jupyterhub/00-gateway-auth.py +++ b/config/jupyterhub/00-gateway-auth.py @@ -18,11 +18,10 @@ import os from pathlib import Path -from urllib.parse import quote, urlencode +from urllib.parse import urlencode -import jupyterhub.handlers.login as _hub_login_handlers -from jupyterhub.handlers.login import LogoutHandler from oauthenticator.generic import GenericOAuthenticator +from oauthenticator.oauth2 import OAuthLogoutHandler def _build_logout_url( @@ -44,7 +43,7 @@ def _build_logout_url( return f"{end_session_url}?{urlencode(params)}" -class KeyCloakLogoutHandler(LogoutHandler): +class KeyCloakLogoutHandler(OAuthLogoutHandler): """Bounce hub logout through Keycloak's end_session endpoint. Hub's local logout only clears its own cookies; KC keeps the user's @@ -52,12 +51,15 @@ class KeyCloakLogoutHandler(LogoutHandler): Pass the user's id_token as ``id_token_hint`` so KC actually terminates the upstream session. - We override ``get()`` (not ``render_logout_page``) because the base - ``LogoutHandler.get`` short-circuits to ``authenticator.logout_redirect_url`` - when ``auto_login=True`` and never invokes ``render_logout_page``. + Override ``render_logout_page`` rather than ``get`` so that + ``LogoutHandler.get`` still runs ``default_handle_logout`` + + ``handle_logout`` (token revocation, cookie clear). For + ``render_logout_page`` to be reached, ``authenticator.logout_redirect_url`` + must be left empty — otherwise ``LogoutHandler.get`` short-circuits + when ``auto_login`` is true. """ - async def get(self): + async def render_logout_page(self): user = self.current_user id_token = None if user is not None: @@ -67,13 +69,9 @@ async def get(self): id_token = auth_state.get("id_token") except Exception: self.log.warning( - "logout: failed reading auth_state for %s — proceeding without id_token_hint", - user.name, exc_info=True, + "logout: failed reading auth_state for %s — proceeding " + "without id_token_hint", user.name, exc_info=True, ) - # Clear hub's local session cookies; LogoutHandler.default_handle_logout - # handles login_user-revocation + stop-server. - await self.default_handle_logout() - await self.handle_logout() url = _build_logout_url( end_session_url=self.authenticator._kc_end_session_url, id_token=id_token, @@ -83,47 +81,20 @@ async def get(self): class KeyCloakOAuthenticator(GenericOAuthenticator): - """Marker subclass so traitlets config can target it explicitly.""" + """Keycloak-flavoured GenericOAuthenticator. + + Swaps in :class:`KeyCloakLogoutHandler` via the ``logout_handler`` + class hook on :class:`oauthenticator.OAuthenticator`, which is what + ``get_handlers`` reads when registering the ``/logout`` route. + """ + + logout_handler = KeyCloakLogoutHandler # Stashed by configure() so the logout handler can build URLs at request time. _kc_end_session_url: str = "" _kc_post_logout_redirect_uri: str = "" -def _install_logout_patch(): - """Replace jupyterhub.handlers.login.LogoutHandler.get with ours. - - The Authenticator's get_handlers list is appended AFTER jupyterhub's - default handlers in init_handlers, so tornado's first-match routing - picks the default LogoutHandler at /logout — our overrides via - get_handlers are dead routes. Monkey-patching the base class is the - least-surprising way to inject id_token_hint without forking the - handler registry. - """ - async def get(self): - user = self.current_user - id_token = None - if user is not None: - try: - auth_state = await user.get_auth_state() - if auth_state: - id_token = auth_state.get("id_token") - except Exception: - self.log.warning( - "logout: failed reading auth_state for %s — proceeding " - "without id_token_hint", user.name, exc_info=True, - ) - await self.default_handle_logout() - await self.handle_logout() - url = _build_logout_url( - end_session_url=self.authenticator._kc_end_session_url, - id_token=id_token, - post_logout_redirect_uri=self.authenticator._kc_post_logout_redirect_uri, - ) - self.redirect(url) - _hub_login_handlers.LogoutHandler.get = get - - def _kc_urls(issuer: str) -> dict: """Derive Keycloak OIDC endpoint URLs from the realm issuer URL.""" base = f"{issuer}/protocol/openid-connect" @@ -166,20 +137,14 @@ def configure( c.KeyCloakOAuthenticator.refresh_pre_spawn = True # Refresh ~1 min before KC's 5-min access-token TTL expires. c.KeyCloakOAuthenticator.auth_refresh_age = 240 - # Hub logout must terminate the upstream Keycloak session. KC v18+ - # requires id_token_hint when post_logout_redirect_uri is given — - # that's per-user, so KeyCloakLogoutHandler builds the URL at request - # time. logout_redirect_url remains set as a fallback for any code - # path that doesn't go through the handler. - c.KeyCloakOAuthenticator.logout_redirect_url = ( - f"{urls['end_session_url']}" - f"?post_logout_redirect_uri={quote(external_url, safe='')}" - ) - # Stash the pieces the logout handler needs at request time. + # Leave logout_redirect_url empty so LogoutHandler.get falls through + # to render_logout_page (our subclass) instead of short-circuiting + # to a static URL that can't include id_token_hint. + c.KeyCloakOAuthenticator.logout_redirect_url = "" + # Stash the pieces KeyCloakLogoutHandler.render_logout_page reads at + # request time to build the per-user end-session URL. c.KeyCloakOAuthenticator._kc_end_session_url = urls["end_session_url"] c.KeyCloakOAuthenticator._kc_post_logout_redirect_uri = external_url - # Patch the base LogoutHandler so /hub/logout terminates the KC session. - _install_logout_patch() # Skip hub's local /hub/login form — go straight to Keycloak's # authorize endpoint. One IdP, no point making the user click a # "Sign in with OAuth 2.0" button. diff --git a/tests/unit/test_keycloak_authenticator.py b/tests/unit/test_keycloak_authenticator.py index 2bfd532..f0a7d57 100644 --- a/tests/unit/test_keycloak_authenticator.py +++ b/tests/unit/test_keycloak_authenticator.py @@ -83,31 +83,22 @@ def test_admin_groups_can_be_overridden_per_deployment(): # Cycle 3 — logout terminates the Keycloak session # --------------------------------------------------------------------------- -def test_logout_redirects_through_keycloak_end_session(): - """Hub logout must hit KC's end-session endpoint, not just clear local cookies. - - Otherwise the next /hub/ hit silently re-uses the live KC session. +def test_configure_leaves_logout_redirect_url_empty_so_handler_runs(): + """LogoutHandler.get short-circuits to authenticator.logout_redirect_url + when auto_login=True, never calling render_logout_page. Keep it empty + so our subclass's render_logout_page (which builds a per-user URL + with id_token_hint) actually fires. """ c, _ = _configure_with_defaults() - url = c.KeyCloakOAuthenticator.logout_redirect_url - assert url.startswith(f"{ISSUER}/protocol/openid-connect/logout"), url + assert c.KeyCloakOAuthenticator.logout_redirect_url == "" -def test_logout_url_includes_post_logout_redirect_uri(): - """After KC sign-out, the browser must return to the hub external URL.""" +def test_configure_stashes_kc_end_session_pieces_for_handler(): + """KeyCloakLogoutHandler reads these at request time.""" c, _ = _configure_with_defaults() - url = c.KeyCloakOAuthenticator.logout_redirect_url - assert "post_logout_redirect_uri=" in url - # URL-encoded form of the external URL must appear (https%3A%2F%2F...) - assert "https%3A%2F%2Fhub.example.test" in url - - -def test_logout_url_url_encodes_external_url_safely(): - """Special characters in external_url must be safely percent-encoded.""" - c, _ = _configure_with_defaults(external_url="https://hub.example.test/?x=1&y=2") - url = c.KeyCloakOAuthenticator.logout_redirect_url - # Raw `&` from the external URL would prematurely terminate the query param. - assert "x%3D1%26y%3D2" in url + kc = c.KeyCloakOAuthenticator + assert kc._kc_end_session_url == f"{ISSUER}/protocol/openid-connect/logout" + assert kc._kc_post_logout_redirect_uri == EXTERNAL # --------------------------------------------------------------------------- @@ -139,16 +130,14 @@ def test_configure_enables_auto_login_so_hub_skips_local_login_form(): assert c.Authenticator.auto_login is True -def test_keycloak_authenticator_serves_custom_logout_handler(): - """KC v18+ requires id_token_hint at end_session_endpoint. - - A static logout_redirect_url can't include it (per-user), so the - authenticator must install a handler that builds the URL per request. +def test_keycloak_authenticator_uses_custom_logout_handler(): + """oauthenticator.OAuthenticator.get_handlers reads the class-level + ``logout_handler`` attribute when registering /logout. Swapping + that to our subclass is the supported way to override logout + behaviour without duplicating the /logout route. """ c, mod = _configure_with_defaults() - handlers = mod.KeyCloakOAuthenticator().get_handlers(app=None) - paths = [h[0] for h in handlers] - assert "/logout" in paths + assert mod.KeyCloakOAuthenticator.logout_handler is mod.KeyCloakLogoutHandler def test_build_logout_url_includes_id_token_hint_and_post_redirect(): From dbf3d4c775184c9654aafffe58daa5f5a367820d Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Fri, 15 May 2026 10:59:14 +0100 Subject: [PATCH 50/60] chore: bump hub image to sha-239effb (logout_handler hook fix) --- values.yaml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/values.yaml b/values.yaml index 02ff5a6..851927b 100644 --- a/values.yaml +++ b/values.yaml @@ -208,12 +208,13 @@ jupyterhub: # Custom Nebari hub image with jhub-apps pre-installed. # Tag below is the PR-53 merge-commit SHA at the moment of pinning; the # corresponding immutable digest is - # sha256:2fdea3efbd07fe8fe426992bb54ee307490d06b521243c32f7f5e95719271e18 + # sha256:61f8bf9bf1ed1a3535d064304be7f2257e3099bd62ed5f2e0e220250f50b6faa # (GenericOAuthenticator + openid scope + starlette<1 + auto_login=True - # + LogoutHandler monkey-patch so /hub/logout sends id_token_hint to KC). + # + KeyCloakLogoutHandler via OAuthenticator.logout_handler hook so + # /hub/logout terminates KC session with id_token_hint). image: name: quay.io/nebari/nebari-data-science-pack-jupyterhub - tag: "sha-8ff8d0a" + tag: "sha-239effb" config: JupyterHub: From 87174a9ddb9719d4505511c9b82fa37614f32629 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Fri, 15 May 2026 11:13:16 +0100 Subject: [PATCH 51/60] fix(auth): stash logout pieces on class attr (not via c. traitlets) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Traitlets' config loader rejects unknown attribute names with a warning and never sets the value, so c.KeyCloakOAuthenticator._kc_end_session_url was a no-op — _kc_end_session_url stayed empty on the class default, making the logout URL relative and causing a redirect loop. --- config/jupyterhub/00-gateway-auth.py | 10 +++++++--- tests/unit/test_keycloak_authenticator.py | 18 ++++++++++++------ 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/config/jupyterhub/00-gateway-auth.py b/config/jupyterhub/00-gateway-auth.py index 0cbc70e..b1441ee 100644 --- a/config/jupyterhub/00-gateway-auth.py +++ b/config/jupyterhub/00-gateway-auth.py @@ -142,9 +142,13 @@ def configure( # to a static URL that can't include id_token_hint. c.KeyCloakOAuthenticator.logout_redirect_url = "" # Stash the pieces KeyCloakLogoutHandler.render_logout_page reads at - # request time to build the per-user end-session URL. - c.KeyCloakOAuthenticator._kc_end_session_url = urls["end_session_url"] - c.KeyCloakOAuthenticator._kc_post_logout_redirect_uri = external_url + # request time to build the per-user end-session URL. These are + # plain class attributes (not traitlets), so set them directly on + # the class instead of via `c.. = ...` — traitlets' + # config-loader rejects unknown names with a warning and never + # propagates the value. + KeyCloakOAuthenticator._kc_end_session_url = urls["end_session_url"] + KeyCloakOAuthenticator._kc_post_logout_redirect_uri = external_url # Skip hub's local /hub/login form — go straight to Keycloak's # authorize endpoint. One IdP, no point making the user click a # "Sign in with OAuth 2.0" button. diff --git a/tests/unit/test_keycloak_authenticator.py b/tests/unit/test_keycloak_authenticator.py index f0a7d57..cf8533e 100644 --- a/tests/unit/test_keycloak_authenticator.py +++ b/tests/unit/test_keycloak_authenticator.py @@ -93,12 +93,18 @@ def test_configure_leaves_logout_redirect_url_empty_so_handler_runs(): assert c.KeyCloakOAuthenticator.logout_redirect_url == "" -def test_configure_stashes_kc_end_session_pieces_for_handler(): - """KeyCloakLogoutHandler reads these at request time.""" - c, _ = _configure_with_defaults() - kc = c.KeyCloakOAuthenticator - assert kc._kc_end_session_url == f"{ISSUER}/protocol/openid-connect/logout" - assert kc._kc_post_logout_redirect_uri == EXTERNAL +def test_configure_stashes_kc_end_session_pieces_on_authenticator_class(): + """KeyCloakLogoutHandler reads these at request time. + + They live on the class (not the traitlets `c.` namespace) because + traitlets' config-loader rejects unknown names with a warning and + never propagates the value into the actual instance. + """ + _, mod = _configure_with_defaults() + assert mod.KeyCloakOAuthenticator._kc_end_session_url == ( + f"{ISSUER}/protocol/openid-connect/logout" + ) + assert mod.KeyCloakOAuthenticator._kc_post_logout_redirect_uri == EXTERNAL # --------------------------------------------------------------------------- From 0550865beea9d120060be5b4e302286042262339 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Fri, 15 May 2026 11:16:16 +0100 Subject: [PATCH 52/60] chore: bump hub image to sha-2c816a2 (logout class-attr fix) --- values.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/values.yaml b/values.yaml index 851927b..b56ab34 100644 --- a/values.yaml +++ b/values.yaml @@ -208,13 +208,13 @@ jupyterhub: # Custom Nebari hub image with jhub-apps pre-installed. # Tag below is the PR-53 merge-commit SHA at the moment of pinning; the # corresponding immutable digest is - # sha256:61f8bf9bf1ed1a3535d064304be7f2257e3099bd62ed5f2e0e220250f50b6faa + # sha256:f70cd65dbee4a2c60a762582c8802d0a8ed6158bbd3481b7e7b3674c330c1b1b # (GenericOAuthenticator + openid scope + starlette<1 + auto_login=True - # + KeyCloakLogoutHandler via OAuthenticator.logout_handler hook so - # /hub/logout terminates KC session with id_token_hint). + # + KeyCloakLogoutHandler set via class-attr so logout URL pieces + # actually propagate — traitlets-config'd private attrs are no-ops). image: name: quay.io/nebari/nebari-data-science-pack-jupyterhub - tag: "sha-239effb" + tag: "sha-2c816a2" config: JupyterHub: From 34e845b30ea2f47ae4ec8104b2610bdcddc51d45 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Fri, 15 May 2026 11:20:20 +0100 Subject: [PATCH 53/60] fix(auth): override LogoutHandler.get to capture id_token before user cleared LogoutHandler.get sets self._jupyterhub_user = None BEFORE calling render_logout_page (jupyterhub/handlers/login.py:89), so reading auth_state from render_logout_page always sees current_user=None. Move the id_token capture into get() before the cleanup runs. --- config/jupyterhub/00-gateway-auth.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/config/jupyterhub/00-gateway-auth.py b/config/jupyterhub/00-gateway-auth.py index b1441ee..3f89a27 100644 --- a/config/jupyterhub/00-gateway-auth.py +++ b/config/jupyterhub/00-gateway-auth.py @@ -46,20 +46,15 @@ def _build_logout_url( class KeyCloakLogoutHandler(OAuthLogoutHandler): """Bounce hub logout through Keycloak's end_session endpoint. - Hub's local logout only clears its own cookies; KC keeps the user's - session alive, so the next /hub/login transparently re-authenticates. - Pass the user's id_token as ``id_token_hint`` so KC actually - terminates the upstream session. - - Override ``render_logout_page`` rather than ``get`` so that - ``LogoutHandler.get`` still runs ``default_handle_logout`` + - ``handle_logout`` (token revocation, cookie clear). For - ``render_logout_page`` to be reached, ``authenticator.logout_redirect_url`` - must be left empty — otherwise ``LogoutHandler.get`` short-circuits - when ``auto_login`` is true. + KC requires ``id_token_hint`` when ``post_logout_redirect_uri`` is + present. The token lives in the user's auth_state, so it must be + read at request time. Override ``get`` (not ``render_logout_page``) + because ``LogoutHandler.get`` clears ``self._jupyterhub_user`` BEFORE + calling ``render_logout_page`` — by the time the latter runs, + ``current_user`` is None and auth_state is unreachable. """ - async def render_logout_page(self): + async def get(self): user = self.current_user id_token = None if user is not None: @@ -72,6 +67,11 @@ async def render_logout_page(self): "logout: failed reading auth_state for %s — proceeding " "without id_token_hint", user.name, exc_info=True, ) + # Standard cleanup from base LogoutHandler.get (token revocation, + # cookie clear, server shutdown). + await self.default_handle_logout() + await self.handle_logout() + self._jupyterhub_user = None url = _build_logout_url( end_session_url=self.authenticator._kc_end_session_url, id_token=id_token, From 71fc4dcf8716e42f7de1d4ed9ba715bcdf536069 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Fri, 15 May 2026 11:23:36 +0100 Subject: [PATCH 54/60] chore: bump hub image to sha-67880ee (logout id_token capture in get) --- values.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/values.yaml b/values.yaml index b56ab34..4f23ddc 100644 --- a/values.yaml +++ b/values.yaml @@ -208,13 +208,13 @@ jupyterhub: # Custom Nebari hub image with jhub-apps pre-installed. # Tag below is the PR-53 merge-commit SHA at the moment of pinning; the # corresponding immutable digest is - # sha256:f70cd65dbee4a2c60a762582c8802d0a8ed6158bbd3481b7e7b3674c330c1b1b + # sha256:7f895be95dd6077f06be35f7ca80cbfe91fa67f4daf27c095b91a2c9c215950f # (GenericOAuthenticator + openid scope + starlette<1 + auto_login=True - # + KeyCloakLogoutHandler set via class-attr so logout URL pieces - # actually propagate — traitlets-config'd private attrs are no-ops). + # + KeyCloakLogoutHandler captures id_token in get() before base + # class clears current_user — id_token_hint now reaches KC). image: name: quay.io/nebari/nebari-data-science-pack-jupyterhub - tag: "sha-2c816a2" + tag: "sha-67880ee" config: JupyterHub: From 66498502d28080621f00d39aba534216f116a803 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Fri, 15 May 2026 14:02:57 +0100 Subject: [PATCH 55/60] fix(spawner): pin pvc_name_template to claim-{username} KubeSpawner's default `pvc_name_template` for a *named* server is `claim-{username}--{servername}`, but the chart's home volume mount is hardcoded to `claim-{username}` (so all of a user's servers share a single RWO PVC, co-located on one node via the pod-affinity rule). Without an explicit override the names diverge: KubeSpawner creates a fresh per-server PVC and the pod tries to mount a different per-user PVC. Users who'd previously launched the default JupyterLab server still had the per-user PVC sitting around and survived; fresh users (e.g. anyone who first interacts with the platform via jhub-apps Create App) hit FailedScheduling: 'persistentvolumeclaim claim- not found' and the pod sits Pending until the hub spawn-timeout (5 min) fires. Lock the template to `claim-{username}` so ensure + mount converge. Test in tests/unit/test_spawner_storage.py. --- config/jupyterhub/01-spawner.py | 9 +++++ tests/unit/test_spawner_storage.py | 56 ++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 tests/unit/test_spawner_storage.py diff --git a/config/jupyterhub/01-spawner.py b/config/jupyterhub/01-spawner.py index 7efebc9..b312cfc 100644 --- a/config/jupyterhub/01-spawner.py +++ b/config/jupyterhub/01-spawner.py @@ -31,6 +31,15 @@ c.KubeSpawner.storage_pvc_ensure = True c.KubeSpawner.storage_capacity = get_config("custom.storage-capacity", "20Gi") c.KubeSpawner.storage_access_modes = ["ReadWriteOnce"] +# Without this override, KubeSpawner's default template is +# `claim-{username}--{servername}`, so for jhub-apps named servers it ensures +# a per-server PVC — while the `volumes` block below mounts the per-user +# `claim-{username}`. Users with a pre-existing per-user PVC (legacy +# JupyterLab launch) survived this mismatch; fresh users hit +# FailedScheduling because the per-user PVC is never auto-created. Forcing +# the template to `{username}` makes ensure + mount converge on the same +# RWO claim, which the pod-affinity rule below keeps to a single node. +c.KubeSpawner.pvc_name_template = "claim-{username}" c.KubeSpawner.volumes = [ { "name": "home", diff --git a/tests/unit/test_spawner_storage.py b/tests/unit/test_spawner_storage.py new file mode 100644 index 0000000..bc2f5c1 --- /dev/null +++ b/tests/unit/test_spawner_storage.py @@ -0,0 +1,56 @@ +"""Tests for the storage wiring in `01-spawner.py`. + +The chart maps every spawn (default JupyterLab AND jhub-apps named servers) +to the same per-user RWO PVC. KubeSpawner has two knobs that have to agree: + + * `pvc_name_template` — what KubeSpawner ensures (creates if missing) + * `volumes[].persistentVolumeClaim.claimName` — what the pod mounts + +Default `pvc_name_template` is `claim-{username}--{servername}` for named +servers, so without an override the chart would create a per-server PVC +while the pod tries to mount a different (per-user) one. Fresh users hit +FailedScheduling: PVC not found. Lock the template to `claim-{username}` so +ensure + mount always converge. +""" + +from __future__ import annotations + +import sys +import types + +# 01-spawner.py imports `z2jh.get_config` for chart-driven values, which +# isn't installed in the host venv. Stub it with a default-returning shim +# so the module can be exec'd in isolation. +_z2jh = types.ModuleType("z2jh") +_z2jh.get_config = lambda key, default=None: default +sys.modules.setdefault("z2jh", _z2jh) + +from conftest import FakeConfig, load_config_module # noqa: E402 + + +def test_pvc_name_template_matches_volume_claim(): + """KubeSpawner.pvc_name_template must match the home volume's claimName. + + Without this guarantee, the chart silently breaks for any user without a + pre-existing per-user PVC — the PVC KubeSpawner ensures has a different + name than the one the pod mounts. + """ + c = FakeConfig() + load_config_module("01-spawner.py", inject_c=c) + + template = getattr(c.KubeSpawner, "pvc_name_template", None) + assert template == "claim-{username}", ( + f"pvc_name_template={template!r} — must be 'claim-{{username}}' so " + f"the ensured PVC matches the home volume's claimName." + ) + + # Cross-check: the home volume must reference the same PVC name. + home_volume = next( + (v for v in c.KubeSpawner.volumes if v.get("name") == "home"), None + ) + assert home_volume is not None, "no 'home' volume defined on KubeSpawner.volumes" + claim = home_volume["persistentVolumeClaim"]["claimName"] + assert claim == "claim-{username}", ( + f"home volume claimName={claim!r} diverges from pvc_name_template; " + f"new users will FailedScheduling on the missing PVC." + ) From 8b8c190e3d2808d31a600e4a8312136795b30d10 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Fri, 15 May 2026 15:00:33 +0100 Subject: [PATCH 56/60] fix(auth): implement refresh_user to rotate KC refresh_token in auth_state The earlier switch to KeyCloakOAuthenticator (GenericOAuthenticator) set `auth_refresh_age = 240`, expecting JupyterHub to keep auth_state fresh via its built-in refresh_user. But JupyterHub's Authenticator.refresh_user is a no-op stub (returns True) and oauthenticator's GenericOAuthenticator does not override it. So auth_state.refresh_token stays frozen at OAuth-callback time and expires after KC's SSO idle timeout (~30 min by default), at which point nebi-envs's 3-step token exchange fails at step 1 with: invalid_grant: Token is not active and the jhub-apps Create-App "Software Environment" dropdown silently disappears (env list is empty when the exchange fails). Implement refresh_user on KeyCloakOAuthenticator: POST grant_type= refresh_token to KC's token endpoint, persist the rotated tokens back to auth_state via the {"auth_state": ...} return shape, return False on invalid_grant to force re-login, and return True (no-op) on transient HTTP errors. Tests in tests/unit/test_refresh_user.py cover the four return-shape contracts: success, invalid_grant, transient error, no-refresh-token. --- config/jupyterhub/00-gateway-auth.py | 73 +++++++++++++ tests/unit/test_refresh_user.py | 149 +++++++++++++++++++++++++++ 2 files changed, 222 insertions(+) create mode 100644 tests/unit/test_refresh_user.py diff --git a/config/jupyterhub/00-gateway-auth.py b/config/jupyterhub/00-gateway-auth.py index 3f89a27..781e997 100644 --- a/config/jupyterhub/00-gateway-auth.py +++ b/config/jupyterhub/00-gateway-auth.py @@ -16,12 +16,14 @@ # ruff: noqa: F821 - `c` is a magic global provided by JupyterHub +import json import os from pathlib import Path from urllib.parse import urlencode from oauthenticator.generic import GenericOAuthenticator from oauthenticator.oauth2 import OAuthLogoutHandler +from tornado.httpclient import HTTPClientError def _build_logout_url( @@ -94,6 +96,77 @@ class hook on :class:`oauthenticator.OAuthenticator`, which is what _kc_end_session_url: str = "" _kc_post_logout_redirect_uri: str = "" + async def refresh_user(self, user, handler=None): + """Run KC's refresh_token grant and persist rotated tokens to auth_state. + + JupyterHub's default Authenticator.refresh_user is a no-op (returns + True), and GenericOAuthenticator doesn't override it. Result: the + refresh_token stored in auth_state at OAuth-callback time stays + frozen until KC's SSO idle timeout invalidates it (~30 min by + default). The next sync caller — nebi-envs's 3-step exchange — + then fails at step 1 with `invalid_grant: Token is not active`, + env list returns [], and the Create-App Software Environment + dropdown vanishes for the user. + + Contract (per JupyterHub Authenticator.refresh_user docstring): + - return dict -> JupyterHub merges these fields into auth_state + - return True -> auth_state is fine, leave it + - return False -> auth_state is invalid, force re-login + + We always return a dict on a successful grant so KC's rotated + refresh_token gets written back. On `invalid_grant` we return False + to force re-auth (silent no-op would leave the session stale until + next manual login). Transient errors keep the existing auth_state. + """ + auth_state = await user.get_auth_state() + if not auth_state or not auth_state.get("refresh_token"): + return True + + body = urlencode({ + "grant_type": "refresh_token", + "client_id": self.client_id, + "client_secret": self.client_secret, + "refresh_token": auth_state["refresh_token"], + }) + try: + token_info = await self.httpfetch( + self.token_url, + method="POST", + headers={"Content-Type": "application/x-www-form-urlencoded"}, + body=body, + ) + except HTTPClientError as e: + err_kind = "" + if e.response is not None and e.response.body: + try: + err_kind = json.loads(e.response.body).get("error", "") + except Exception: + pass + if e.code == 400 and err_kind == "invalid_grant": + self.log.warning( + "KC refresh_token expired for %s — forcing re-login", + user.name, + ) + return False + self.log.warning( + "KC refresh failed for %s: HTTP %s %s — keeping current auth_state", + user.name, e.code, err_kind or e.message, + ) + return True + + # Successful grant: KC returns a new access_token (always) and a + # rotated refresh_token (when rotation enabled). Preserve any + # auth_state keys we don't get back (e.g. oauth_user from + # GenericOAuthenticator.authenticate's response). + new_state = dict(auth_state) + new_state["access_token"] = token_info["access_token"] + if token_info.get("refresh_token"): + new_state["refresh_token"] = token_info["refresh_token"] + if token_info.get("id_token"): + new_state["id_token"] = token_info["id_token"] + new_state["token_response"] = token_info + return {"auth_state": new_state} + def _kc_urls(issuer: str) -> dict: """Derive Keycloak OIDC endpoint URLs from the realm issuer URL.""" diff --git a/tests/unit/test_refresh_user.py b/tests/unit/test_refresh_user.py new file mode 100644 index 0000000..c453411 --- /dev/null +++ b/tests/unit/test_refresh_user.py @@ -0,0 +1,149 @@ +"""Tests for KeyCloakOAuthenticator.refresh_user. + +JupyterHub calls `refresh_user(user)` every `auth_refresh_age` seconds. The +default Authenticator.refresh_user is a no-op (returns True), so without +this override the chart's stored `auth_state.refresh_token` is never +rotated and expires after Keycloak's SSO idle timeout (~30 min by +default). nebi-envs's 3-step exchange then fails at step 1 with +`invalid_grant: Token is not active`, env list silently returns [], and +the Create-App Software Environment dropdown disappears. + +These tests pin the contract for that override. +""" + +from __future__ import annotations + +import asyncio +import json +import sys +import types +from unittest.mock import AsyncMock + +import pytest +from io import BytesIO + +from tornado.httpclient import HTTPClientError, HTTPResponse, HTTPRequest + +# 00-gateway-auth.py imports cleanly without a chart `c` config object. +import importlib.util +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[2] +MODULE_PATH = REPO_ROOT / "config" / "jupyterhub" / "00-gateway-auth.py" +spec = importlib.util.spec_from_file_location("_gateway_auth", MODULE_PATH) +ga = importlib.util.module_from_spec(spec) +spec.loader.exec_module(ga) + + +class FakeUser: + """Stand-in for jupyterhub.user.User exposing the bits refresh_user reads.""" + + def __init__(self, name: str, auth_state: dict): + self.name = name + self._auth_state = auth_state + + async def get_auth_state(self): + return self._auth_state + + +def _make_authenticator() -> ga.KeyCloakOAuthenticator: + """KC authenticator pre-wired with the URLs refresh_user needs.""" + auth = ga.KeyCloakOAuthenticator() + auth.token_url = "https://kc.test/realms/r/protocol/openid-connect/token" + auth.client_id = "cid" + auth.client_secret = "sek" + return auth + + +@pytest.fixture +def auth(): + return _make_authenticator() + + +def _run(coro): + return asyncio.get_event_loop().run_until_complete(coro) + + +def test_refresh_user_returns_updated_auth_state_on_success(auth): + """A successful refresh-token grant must surface as a dict so JupyterHub + persists the new tokens (and KC's rotated refresh_token) back to the DB. + Returning True would silently drop the rotation.""" + user = FakeUser("alice", { + "access_token": "old-at", + "refresh_token": "old-rt", + "id_token": "old-id", + }) + auth.httpfetch = AsyncMock(return_value={ + "access_token": "new-at", + "refresh_token": "new-rt", + "id_token": "new-id", + "expires_in": 300, + }) + + result = _run(auth.refresh_user(user)) + + assert isinstance(result, dict), ( + f"refresh_user returned {result!r}; must return a dict so JupyterHub " + "writes the rotated refresh_token back to auth_state." + ) + new_state = result["auth_state"] + assert new_state["access_token"] == "new-at" + assert new_state["refresh_token"] == "new-rt" + assert new_state["id_token"] == "new-id" + + # httpfetch must hit the KC token endpoint with grant_type=refresh_token + call_kwargs = auth.httpfetch.call_args.kwargs + assert "/token" in auth.httpfetch.call_args.args[0] + body = call_kwargs.get("body") or auth.httpfetch.call_args.args[1] + assert "grant_type=refresh_token" in body + assert "refresh_token=old-rt" in body + + +def test_refresh_user_returns_false_on_invalid_grant(auth): + """KC returns 400 invalid_grant when the refresh_token has expired or + been revoked. The user must be sent back through the auth flow — return + False so JupyterHub forces a re-login on the next request.""" + user = FakeUser("alice", {"refresh_token": "expired-rt"}) + err_body = json.dumps({ + "error": "invalid_grant", + "error_description": "Token is not active", + }).encode() + fake_resp = HTTPResponse( + HTTPRequest(auth.token_url), 400, buffer=BytesIO(err_body), + ) + auth.httpfetch = AsyncMock( + side_effect=HTTPClientError(400, "Bad Request", fake_resp) + ) + + result = _run(auth.refresh_user(user)) + + assert result is False, ( + f"got {result!r}; invalid_grant must return False so JupyterHub " + "drops the session and re-authenticates the user." + ) + + +def test_refresh_user_returns_true_on_transient_error(auth): + """Network blips, 5xx, etc. shouldn't log the user out — return True + and let the next refresh tick try again.""" + user = FakeUser("alice", {"refresh_token": "rt"}) + auth.httpfetch = AsyncMock( + side_effect=HTTPClientError(503, "Service Unavailable") + ) + + result = _run(auth.refresh_user(user)) + + assert result is True + + +def test_refresh_user_returns_true_when_no_refresh_token(auth): + """If auth_state has no refresh_token (legacy session, Envoy-era), we + can't refresh — but we also shouldn't log the user out unprompted. + Leave it for the next request's normal auth check to handle.""" + user = FakeUser("alice", {"access_token": "at-only"}) + auth.httpfetch = AsyncMock() + + result = _run(auth.refresh_user(user)) + + assert result is True + auth.httpfetch.assert_not_called() From 19801eaee76b511fd320c73d9e29a9d477998ad0 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Fri, 15 May 2026 17:16:31 +0100 Subject: [PATCH 57/60] fix(e2e): drop japps service_workers 4 -> 1 to unstick hub bootup z2jh's hub waits ~10s for each managed service's HTTP port to bind. The default jhub-apps service_workers is 4 and four uvicorn workers take ~12s to bind on CI runners, so hub crashes with Cannot connect to managed service japps at http://hub:10202 restarts, hits the same timeout, restarts, etc. By the time the e2e fixture's port-forward starts polling /hub/login, hub is still in this crash-loop. The first urlopen with timeout=15s eventually raises TimeoutError unwrapped through HubClient._request (which only catches HTTPError), aborting every test fixture in setup. Mirror the production overlay (gitops/apps/data-science-pack.yaml in openteams-ai/nebari-hetzner) which pins service_workers: 1. The hub boots cleanly within seconds and the e2e suite proceeds. --- tests/e2e/fixtures/test-values.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/e2e/fixtures/test-values.yaml b/tests/e2e/fixtures/test-values.yaml index 6fa0645..2583a84 100644 --- a/tests/e2e/fixtures/test-values.yaml +++ b/tests/e2e/fixtures/test-values.yaml @@ -31,6 +31,14 @@ jupyterhub: custom: shared-storage-enabled: true shared-storage-mount-prefix: "/shared" + # Hub's wait_for_http_server timeout for managed services is hardcoded + # to 10s. Default japps_workers=4 takes ~12s to bind on CI runners, so + # hub crashes with "Cannot connect to managed service japps" on every + # start and never finishes booting before the test harness gives up. + # Mirror the prod overlay (gitops/apps/data-science-pack.yaml in + # openteams-ai/nebari-hetzner). + japps-config: + service_workers: 1 hub: config: From 65c51eb048e7acef033cca536347d9a09dbdfa3a Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Fri, 15 May 2026 18:03:41 +0100 Subject: [PATCH 58/60] chore: remove EnvoyOIDC-era stale-token fallbacks made dead by refresh_user MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `if access_token and not refresh_token` branch in 01-spawner.py's _nebi_pre_spawn_hook and 03-nebi-envs.py's get_nebi_environments was a fallback for EnvoyOIDCAuthenticator's auth_state, which never carried a refresh_token (Envoy only stored access_token + id_token in cookies). With KeyCloakOAuthenticator + the new refresh_user() override, auth_state always has a rotating refresh_token, so the branch never fires. Also remove the now-orphan `_fetch_fresh_auth_state` helper it called, and update two docstrings/comments that still referenced EnvoyOIDCAuthenticator as the source of the groups claim or the hub's OAuth client. Reword values.yaml comment on `forwardAccessToken: false` to drop the "avoids confusing dual-token paths" framing — there is no Envoy-injected Bearer in the current architecture. --- config/jupyterhub/01-spawner.py | 55 +++---------------------------- config/jupyterhub/03-nebi-envs.py | 20 ----------- values.yaml | 5 ++- 3 files changed, 7 insertions(+), 73 deletions(-) diff --git a/config/jupyterhub/01-spawner.py b/config/jupyterhub/01-spawner.py index b312cfc..dc0ea38 100644 --- a/config/jupyterhub/01-spawner.py +++ b/config/jupyterhub/01-spawner.py @@ -401,32 +401,6 @@ def get_nebi_jwt(refresh_token, access_token, keycloak_url, nebi_cid, hub_cid, h return nebi_jwt -def _fetch_fresh_auth_state(username): - """Fetch the latest auth_state for a user via the JupyterHub API. - - Uses JUPYTERHUB_API_TOKEN (which has admin:auth_state scope) to read - the user's auth_state, which refresh_user() keeps updated with fresh - Envoy cookies on browser requests. - - Returns the auth_state dict, or None on failure. - """ - api_url = os.environ.get("JUPYTERHUB_API_URL", "") - api_token = os.environ.get("JUPYTERHUB_API_TOKEN", "") - if not api_url or not api_token: - log.warning("JUPYTERHUB_API_URL/TOKEN not set, cannot fetch fresh auth_state") - return None - try: - req = Request( - f"{api_url}/users/{username}", - headers={"Authorization": f"token {api_token}"}, - ) - with urlopen(req, timeout=10) as resp: - return json.loads(resp.read()).get("auth_state") - except Exception as exc: - log.error("Failed to fetch fresh auth_state for %s: %s", username, exc) - return None - - # --------------------------------------------------------------------------- # Nebi auto-authentication (pre-spawn hook) # --------------------------------------------------------------------------- @@ -464,26 +438,6 @@ async def _nebi_pre_spawn_hook(spawner): refresh_token = auth_state.get("refresh_token") access_token = auth_state.get("access_token") or "" - # The access token from auth_state may be stale (refresh_user updates - # it on browser requests, but the spawn is an internal API call). - # Check expiry and re-fetch from the hub API if needed. - if access_token and not refresh_token: - claims = _decode_jwt_claims(access_token) - import time as _time - exp = claims.get("exp", 0) - remaining = exp - int(_time.time()) if exp else 0 - if remaining < 30: - log.info( - "Access token for %s expires in %ds, fetching fresh auth_state from hub API", - spawner.user.name, remaining, - ) - fresh_state = await asyncio.to_thread( - _fetch_fresh_auth_state, spawner.user.name, - ) - if fresh_state: - access_token = fresh_state.get("access_token") or access_token - refresh_token = fresh_state.get("refresh_token") or refresh_token - if not refresh_token and not access_token: log.warning("No access_token or refresh_token for %s, skipping", spawner.user.name) return @@ -570,9 +524,10 @@ async def _nebi_pre_spawn_hook(spawner): def _get_user_groups(auth_state): """Extract and filter user groups from auth_state. - Reads groups stored in auth_state by EnvoyOIDCAuthenticator (from Keycloak - IdToken groups claim). Applies the allowlist if configured. - Uses Path(g).name (last component) like classic Nebari so /projects/myproj → myproj. + Reads groups stored in auth_state by KeyCloakOAuthenticator (from + Keycloak's IdToken `groups` claim). Applies the allowlist if + configured. Uses Path(g).name (last component) like classic Nebari so + /projects/myproj → myproj. Deduplicates to prevent duplicate mountPath entries in the pod spec. Note: does NOT fall back to spawner.user.groups — accessing that SQLAlchemy relationship from an async hook causes DetachedInstanceError. @@ -767,7 +722,7 @@ async def _pre_spawn_hook(spawner): else: log.debug("pre-spawn: Nebi auto-auth not configured, skipping") - # 2. Resolve groups from auth_state (stored by EnvoyOIDCAuthenticator) + # 2. Resolve groups from auth_state (stored by KeyCloakOAuthenticator) groups = _get_user_groups(auth_state) log.info("pre-spawn: user %s resolved groups: %s", username, groups) diff --git a/config/jupyterhub/03-nebi-envs.py b/config/jupyterhub/03-nebi-envs.py index 278f0ed..ff2c086 100644 --- a/config/jupyterhub/03-nebi-envs.py +++ b/config/jupyterhub/03-nebi-envs.py @@ -103,26 +103,6 @@ def get_nebi_environments(user): refresh_token = auth_state.get("refresh_token") access_token = auth_state.get("access_token", "") - # auth_state from EnvoyOIDCAuthenticator carries no refresh_token (Envoy - # only stores access_token + id_token in cookies), and the access_token - # lifetime is short (~5min). When jhub-apps calls this synchronously, the - # access_token may already be expired. Mirror 01-spawner.py: if expiring - # in <30s, re-fetch via the hub API which has the freshest cookie state. - if access_token and not refresh_token: - claims = _decode_jwt_claims(access_token) - import time as _time - exp = claims.get("exp", 0) - remaining = exp - int(_time.time()) if exp else 0 - if remaining < 30: - log.info( - "nebi-envs: access_token for %s expires in %ds, re-fetching auth_state", - username, remaining, - ) - fresh_state = _fetch_fresh_auth_state(username) - if fresh_state: - access_token = fresh_state.get("access_token") or access_token - refresh_token = fresh_state.get("refresh_token") or refresh_token - if not refresh_token and not access_token: log.warning( "nebi-envs: no access_token or refresh_token for user %s, cannot list environments", diff --git a/values.yaml b/values.yaml index 4f23ddc..74dd89d 100644 --- a/values.yaml +++ b/values.yaml @@ -37,9 +37,8 @@ nebariapp: # deleteSecurityPolicyIfExists; the KC client + Secret stay provisioned # because provisionClient: true is independent of enforcement. enforceAtGateway: false - # No longer needed once hub owns the OAuth flow — keeping it false avoids - # confusing dual-token paths between Envoy-injected Bearer and hub's own - # stored access_token. + # Hub owns the OAuth flow (KeyCloakOAuthenticator) and persists tokens to + # auth_state. No upstream component needs the Envoy-injected Bearer. forwardAccessToken: false # landingPage controls whether and how this service appears on the Nebari # landing page (requires nebari-operator with LandingPageConfig support). From 81b01a8b25cab503402c611c5750b0e8edd61e26 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Fri, 15 May 2026 18:07:19 +0100 Subject: [PATCH 59/60] refactor(auth): bundle KC strings into KeyCloakConfig dataclass The KC migration left two related concerns scattered: * endpoint URLs (authorize/token/userdata/end_session) derived from the issuer were assigned individually to traitlets in configure(), via a small `_kc_urls` dict helper. * the per-user logout URL had its inputs spread across the free function `_build_logout_url(end_session_url=, id_token=, post_logout_redirect_uri=)` and TWO stray class attributes (`KeyCloakOAuthenticator._kc_end_session_url`, `KeyCloakOAuthenticator._kc_post_logout_redirect_uri`) that configure() stashed at startup so the logout handler could read them at request time. Replace both with a single `KeyCloakConfig` frozen dataclass that holds every KC string the chart needs and owns the logout-URL composition as a method. * `KeyCloakConfig.build(issuer=..., post_logout_redirect_uri=...)` derives every endpoint URL from the realm issuer. * `cfg.build_logout_url(id_token)` composes the end-session URL, omitting `id_token_hint` for legacy sessions (KC v18+ rejects logout without it when `post_logout_redirect_uri` is set). * configure() builds one KeyCloakConfig and stashes it on `KeyCloakOAuthenticator.kc_config`; the logout handler reads it via `self.authenticator.kc_config`. Net effect: one cohesive object instead of two stray class attrs + one free function + one dict helper. The endpoint derivation becomes trivially testable in isolation. Tests updated in test_keycloak_authenticator.py: * `test_configure_attaches_kc_config_to_authenticator_class` replaces the pair of stray-attribute assertions. * `test_kc_config_build_logout_url_*` cover the method directly. * `test_kc_config_from_issuer_is_pure_and_doesnt_need_configure` pins the classmethod's pure-function semantics. --- config/jupyterhub/00-gateway-auth.py | 116 +++++++++++++--------- tests/unit/test_keycloak_authenticator.py | 76 +++++++++----- 2 files changed, 119 insertions(+), 73 deletions(-) diff --git a/config/jupyterhub/00-gateway-auth.py b/config/jupyterhub/00-gateway-auth.py index 781e997..f47adad 100644 --- a/config/jupyterhub/00-gateway-auth.py +++ b/config/jupyterhub/00-gateway-auth.py @@ -18,6 +18,7 @@ import json import os +from dataclasses import dataclass from pathlib import Path from urllib.parse import urlencode @@ -26,23 +27,54 @@ from tornado.httpclient import HTTPClientError -def _build_logout_url( - *, - end_session_url: str, - id_token: str | None, - post_logout_redirect_uri: str, -) -> str: - """Build a KC end-session URL with id_token_hint + post_logout_redirect_uri. - - Keycloak v18+ rejects logout without ``id_token_hint`` when a - ``post_logout_redirect_uri`` is also given. ``id_token`` may be None - if the user's auth_state was never populated (legacy session); fall - back to just the redirect. +@dataclass(frozen=True) +class KeyCloakConfig: + """All Keycloak strings the authenticator + handlers need at runtime. + + Replaces an earlier pattern where logout pieces lived as stray class + attributes on the authenticator (``_kc_end_session_url`` and + ``_kc_post_logout_redirect_uri``) while every other endpoint URL was + set independently on traitlets. Bundling them lets ``configure()`` + derive everything from the issuer in one place and means the logout + handler only has to grab one object off the authenticator instance. + + Use ``KeyCloakConfig.build(issuer=…, post_logout_redirect_uri=…)`` — + the constructor takes already-derived URLs so tests can build odd + shapes directly. """ - params = {"post_logout_redirect_uri": post_logout_redirect_uri} - if id_token: - params["id_token_hint"] = id_token - return f"{end_session_url}?{urlencode(params)}" + + issuer: str + authorize_url: str + token_url: str + userdata_url: str + end_session_url: str + post_logout_redirect_uri: str + + @classmethod + def build(cls, *, issuer: str, post_logout_redirect_uri: str) -> "KeyCloakConfig": + """Derive every KC endpoint URL from the realm issuer.""" + base = f"{issuer}/protocol/openid-connect" + return cls( + issuer=issuer, + authorize_url=f"{base}/auth", + token_url=f"{base}/token", + userdata_url=f"{base}/userinfo", + end_session_url=f"{base}/logout", + post_logout_redirect_uri=post_logout_redirect_uri, + ) + + def build_logout_url(self, id_token: str | None) -> str: + """Compose the per-user KC end-session URL. + + Keycloak v18+ rejects logout without ``id_token_hint`` when a + ``post_logout_redirect_uri`` is also given. ``id_token`` may be + None if the user's auth_state was never populated (legacy + session); fall back to just the redirect. + """ + params = {"post_logout_redirect_uri": self.post_logout_redirect_uri} + if id_token: + params["id_token_hint"] = id_token + return f"{self.end_session_url}?{urlencode(params)}" class KeyCloakLogoutHandler(OAuthLogoutHandler): @@ -74,12 +106,7 @@ async def get(self): await self.default_handle_logout() await self.handle_logout() self._jupyterhub_user = None - url = _build_logout_url( - end_session_url=self.authenticator._kc_end_session_url, - id_token=id_token, - post_logout_redirect_uri=self.authenticator._kc_post_logout_redirect_uri, - ) - self.redirect(url) + self.redirect(self.authenticator.kc_config.build_logout_url(id_token)) class KeyCloakOAuthenticator(GenericOAuthenticator): @@ -92,9 +119,11 @@ class hook on :class:`oauthenticator.OAuthenticator`, which is what logout_handler = KeyCloakLogoutHandler - # Stashed by configure() so the logout handler can build URLs at request time. - _kc_end_session_url: str = "" - _kc_post_logout_redirect_uri: str = "" + # Populated by configure() at startup. Logout + refresh_user read this + # off the authenticator instance at request time. Class attribute (not + # traitlet) because traitlets' config-loader rejects unknown names + # via `c..` assignment. + kc_config: "KeyCloakConfig | None" = None async def refresh_user(self, user, handler=None): """Run KC's refresh_token grant and persist rotated tokens to auth_state. @@ -168,17 +197,6 @@ async def refresh_user(self, user, handler=None): return {"auth_state": new_state} -def _kc_urls(issuer: str) -> dict: - """Derive Keycloak OIDC endpoint URLs from the realm issuer URL.""" - base = f"{issuer}/protocol/openid-connect" - return { - "authorize_url": f"{base}/auth", - "token_url": f"{base}/token", - "userdata_url": f"{base}/userinfo", - "end_session_url": f"{base}/logout", - } - - def configure( c, *, @@ -190,14 +208,16 @@ def configure( admin_groups=None, ): """Wire KeyCloakOAuthenticator onto JupyterHub's `c` config object.""" - urls = _kc_urls(issuer) + kc_config = KeyCloakConfig.build( + issuer=issuer, post_logout_redirect_uri=external_url, + ) c.JupyterHub.authenticator_class = KeyCloakOAuthenticator c.KeyCloakOAuthenticator.client_id = client_id c.KeyCloakOAuthenticator.client_secret = client_secret c.KeyCloakOAuthenticator.oauth_callback_url = callback_url - c.KeyCloakOAuthenticator.authorize_url = urls["authorize_url"] - c.KeyCloakOAuthenticator.token_url = urls["token_url"] - c.KeyCloakOAuthenticator.userdata_url = urls["userdata_url"] + c.KeyCloakOAuthenticator.authorize_url = kc_config.authorize_url + c.KeyCloakOAuthenticator.token_url = kc_config.token_url + c.KeyCloakOAuthenticator.userdata_url = kc_config.userdata_url c.KeyCloakOAuthenticator.username_claim = "preferred_username" # Explicit scopes — GenericOAuthenticator defaults to [] which omits the # scope param entirely; KC then issues a token without `openid` and @@ -214,19 +234,17 @@ def configure( # to render_logout_page (our subclass) instead of short-circuiting # to a static URL that can't include id_token_hint. c.KeyCloakOAuthenticator.logout_redirect_url = "" - # Stash the pieces KeyCloakLogoutHandler.render_logout_page reads at - # request time to build the per-user end-session URL. These are - # plain class attributes (not traitlets), so set them directly on - # the class instead of via `c.. = ...` — traitlets' - # config-loader rejects unknown names with a warning and never - # propagates the value. - KeyCloakOAuthenticator._kc_end_session_url = urls["end_session_url"] - KeyCloakOAuthenticator._kc_post_logout_redirect_uri = external_url + # Stash all KC strings (issuer-derived URLs + post_logout redirect) on + # the class so KeyCloakLogoutHandler can compose per-user end-session + # URLs at request time. Class attribute (not traitlet) because + # traitlets' config-loader rejects unknown names assigned via + # `c..` with a warning and silently drops the value. + KeyCloakOAuthenticator.kc_config = kc_config # Skip hub's local /hub/login form — go straight to Keycloak's # authorize endpoint. One IdP, no point making the user click a # "Sign in with OAuth 2.0" button. c.Authenticator.auto_login = True - # Any KC-authenticated user is admitted (matches prior EnvoyOIDC policy); + # Any KC-authenticated user is admitted (matches the prior policy); # tighten via admin_groups / allowed_groups per-deploy if needed. c.Authenticator.allow_all = True diff --git a/tests/unit/test_keycloak_authenticator.py b/tests/unit/test_keycloak_authenticator.py index cf8533e..84b518b 100644 --- a/tests/unit/test_keycloak_authenticator.py +++ b/tests/unit/test_keycloak_authenticator.py @@ -93,18 +93,26 @@ def test_configure_leaves_logout_redirect_url_empty_so_handler_runs(): assert c.KeyCloakOAuthenticator.logout_redirect_url == "" -def test_configure_stashes_kc_end_session_pieces_on_authenticator_class(): - """KeyCloakLogoutHandler reads these at request time. - - They live on the class (not the traitlets `c.` namespace) because - traitlets' config-loader rejects unknown names with a warning and - never propagates the value into the actual instance. +def test_configure_attaches_kc_config_to_authenticator_class(): + """KeyCloakLogoutHandler reads the bundled config at request time. + + The KeyCloakConfig dataclass replaces the historical pair of stray + class attributes (`_kc_end_session_url`, `_kc_post_logout_redirect_uri`) + with one cohesive object that derives all KC endpoint URLs from the + issuer and carries the post-logout redirect. It lives on the class + (not the traitlets `c.` namespace) because traitlets' config-loader + rejects unknown names with a warning and never propagates the value + into the actual instance. """ _, mod = _configure_with_defaults() - assert mod.KeyCloakOAuthenticator._kc_end_session_url == ( - f"{ISSUER}/protocol/openid-connect/logout" - ) - assert mod.KeyCloakOAuthenticator._kc_post_logout_redirect_uri == EXTERNAL + cfg = mod.KeyCloakOAuthenticator.kc_config + assert cfg is not None, "configure() must populate kc_config" + assert cfg.issuer == ISSUER + assert cfg.end_session_url == f"{ISSUER}/protocol/openid-connect/logout" + assert cfg.token_url == f"{ISSUER}/protocol/openid-connect/token" + assert cfg.authorize_url == f"{ISSUER}/protocol/openid-connect/auth" + assert cfg.userdata_url == f"{ISSUER}/protocol/openid-connect/userinfo" + assert cfg.post_logout_redirect_uri == EXTERNAL # --------------------------------------------------------------------------- @@ -146,32 +154,52 @@ def test_keycloak_authenticator_uses_custom_logout_handler(): assert mod.KeyCloakOAuthenticator.logout_handler is mod.KeyCloakLogoutHandler -def test_build_logout_url_includes_id_token_hint_and_post_redirect(): - c, mod = _configure_with_defaults() - url = mod._build_logout_url( - end_session_url=f"{ISSUER}/protocol/openid-connect/logout", - id_token="header.payload.signature", - post_logout_redirect_uri=EXTERNAL, - ) +def test_kc_config_build_logout_url_includes_id_token_hint_and_post_redirect(): + """KeyCloakConfig.build_logout_url owns the end-session URL composition + so the logout handler only has to fetch the per-user id_token.""" + _, mod = _configure_with_defaults() + cfg = mod.KeyCloakOAuthenticator.kc_config + url = cfg.build_logout_url(id_token="header.payload.signature") assert url.startswith(f"{ISSUER}/protocol/openid-connect/logout?") assert "id_token_hint=header.payload.signature" in url assert "post_logout_redirect_uri=" in url assert "https%3A%2F%2Fhub.example.test" in url -def test_build_logout_url_omits_id_token_hint_when_missing(): +def test_kc_config_build_logout_url_omits_id_token_hint_when_missing(): """Token may be absent (legacy session): still produce a usable URL that at least clears local cookies and bounces back.""" - c, mod = _configure_with_defaults() - url = mod._build_logout_url( - end_session_url=f"{ISSUER}/protocol/openid-connect/logout", - id_token=None, - post_logout_redirect_uri=EXTERNAL, - ) + _, mod = _configure_with_defaults() + cfg = mod.KeyCloakOAuthenticator.kc_config + url = cfg.build_logout_url(id_token=None) assert "id_token_hint=" not in url assert "post_logout_redirect_uri=" in url +def test_kc_config_from_issuer_is_pure_and_doesnt_need_configure(): + """The endpoint derivation is independent of `configure()`; callers + can build a KeyCloakConfig directly for testing or alternate setups.""" + import importlib.util + from pathlib import Path + spec = importlib.util.spec_from_file_location( + "_ga", + Path(__file__).resolve().parents[2] / "config" / "jupyterhub" / "00-gateway-auth.py", + ) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + + cfg = mod.KeyCloakConfig.build( + issuer="https://kc/realms/r", + post_logout_redirect_uri="https://app/", + ) + base = "https://kc/realms/r/protocol/openid-connect" + assert cfg.token_url == f"{base}/token" + assert cfg.authorize_url == f"{base}/auth" + assert cfg.userdata_url == f"{base}/userinfo" + assert cfg.end_session_url == f"{base}/logout" + assert cfg.post_logout_redirect_uri == "https://app/" + + def test_any_keycloak_authenticated_user_is_allowed_by_default(): """The gateway path admitted any KC user; this path must match. From 37e7626aee3a12b69a779515b6357ca30145c454 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Fri, 15 May 2026 19:44:40 +0100 Subject: [PATCH 60/60] chore: untrack HANDOFF-stale-token.md and ignore future handoff notes HANDOFF*.md files are in-flight working notes between agent sessions; they should never have been checked in. Remove the one that snuck in during the auth saga and add a .gitignore rule so the next one doesn't. --- .gitignore | 3 + HANDOFF-stale-token.md | 138 ----------------------------------------- 2 files changed, 3 insertions(+), 138 deletions(-) delete mode 100644 HANDOFF-stale-token.md diff --git a/.gitignore b/.gitignore index 26390c1..b6e9667 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ # Helm dependencies (regenerated via helm dependency update) charts/ +# Working handoff notes — never commit. +HANDOFF*.md + # OS .DS_Store diff --git a/HANDOFF-stale-token.md b/HANDOFF-stale-token.md deleted file mode 100644 index 3895ba2..0000000 --- a/HANDOFF-stale-token.md +++ /dev/null @@ -1,138 +0,0 @@ -# RESOLVED 2026-05-15 — switched hub to GenericOAuthenticator - -The env-list stale-token symptom is gone in production. Hub now runs -JupyterHub's `KeyCloakOAuthenticator(GenericOAuthenticator)` instead of -reading Envoy-set cookies; the built-in `refresh_user` runs the -refresh_token grant on `auth_refresh_age` (240s) so the stored -access_token never goes stale. - -**Proof from hub log on hetzner**: - -``` -23:28:27 token-exchange step 1: refresh succeeded — valid for 300s -23:34:29 token-exchange step 1: refresh succeeded — valid for 300s (6 min later) -nebi-envs: listed 0 ready environments (test user has no workspaces) -``` - -Pre-fix the 23:34 probe would have returned `EXPIRED` and `400 -invalid_request` from KC at step 2. - -**Landed PRs**: -- nebari-data-science-pack #53 — `KeyCloakOAuthenticator`, chart wiring, - `enforceAtGateway: false` for hub, starlette<1 cap, jhub-apps 2025.11.1 - (off the git pin), unit tests. -- hetzner-nebari main (PR #1) — Secret mount, OAUTH_* env vars, - service_workers: 1 to fit under hub's 10s managed-service timeout, - JUPYTERHUB_OIDC_CLIENT_SECRET re-exposed as env for 03-nebi-envs.py. - -The old design notes below are kept for the archive; they describe the -symptom + the dead-ends we ruled out (cache nebi JWT, plumb Bearer -through jhub-apps) before landing the GenericOAuthenticator switch. - ---- - -# Stale access-token in jhub-apps env selector — handoff - -## Symptom - -`/services/japps/conda-environments/` returns `[]` ~5 min after a fresh Keycloak login, so the **Software Environment** dropdown on the *Create App* page disappears. Refreshing the page does not help. Hard-clearing cookies + logging in again restores it for another ~5 min. - -## What works today - -End-to-end chain is wired correctly: - -| Layer | Confirmed | -|-------|-----------| -| Keycloak v26.5 standard token exchange (V2) on `jupyterhub-…` client | `standard.token.exchange.enabled=true` (operator) | -| Audience mapper for `nebi-…` on hub client | `oidc-audience-mapper` (operator) | -| Hub client allowed to exchange to nebi audience | yes | -| Nebi accepts the resulting JWT | yes | -| ds-pack chart passthrough for `enforceAtGateway` / `forwardAccessToken` | yes | -| `_extract_envoy_cookies` reads `Authorization: Bearer` first, cookie fallback | yes (PR #53) | -| Stale-token re-fetch in `03-nebi-envs.py` | yes (PR #53) | - -## Why it still breaks at ~5 min - -Hub stores per-user `auth_state` (id_token + access_token) in its DB at OAuth callback time. After that: - -- Envoy Gateway v1.6 **does not rotate the `AccessToken-*` cookie content** even with `oidc.refreshToken: true` set on the SecurityPolicy. Envoy refreshes the access token internally (server-side) but only updates cookies in some narrow paths — not on every upstream request. -- Hub's `EnvoyOIDCAuthenticator.refresh_user` only fires when JupyterHub itself receives a request **with a `handler` AND with the IdToken cookie**. jhub-apps service requests to `/services/japps/*` go to japps, not hub, so they never trigger `refresh_user`. Hub's `auth_state` stays at whatever was captured at last `/hub/*` browser hit. -- The env-listing callable (`get_nebi_environments`) reads `user["auth_state"]` via the hub API, which returns the stale stored value — there is no Hub API endpoint that "refresh and return". The recent `_fetch_fresh_auth_state` mitigation in `03-nebi-envs.py` is a no-op because the hub's stored value is itself stale. -- Default Keycloak access-token lifespan is **5 min**. After that, Keycloak rejects the token at step 2 of token exchange with `400 invalid_request "Invalid token"` and the callable returns `[]`. - -## What we tried and ruled out - -### `forwardAccessToken: true` on the SecurityPolicy -Makes Envoy inject `Authorization: Bearer ` on every upstream request, fixing the freshness problem. **But** jhub-apps's `service.security.get_current_user` reads the `Authorization` header **before** its own cookie: - -```python -auth_header = OAuth2AuthorizationCodeBearer(...) # <-- reads Authorization -... -token = auth_param or auth_header or auth_cookie -token = _get_jhub_token_from_jwt_token(token) # tries HS256 decode -``` - -The Keycloak access token is RS256, so `jwt.decode(..., algorithms=["HS256"])` throws `InvalidAlgorithmError`, jhub-apps returns 401, the browser is redirected to `/jhub-login`, OAuth round-trips, sets a new cookie, and the next request hits the same path — **infinite loop in the UI**. - -`forwardAccessToken` is therefore defaulted to `false` in `values.yaml` until jhub-apps is patched. - -### Cache `workspace_list` for 10 min (`87cd6c7`, reverted in `ec6f135`) -Hides the symptom for 10 min, but the next request after the cache expires still requires a fresh access token, which is no longer fresh — same failure mode at a longer interval. - -### Cache the **Nebi JWT** for 24 h -The third leg of the exchange returns a Nebi JWT with a 24 h lifetime. Caching that per user means token exchange runs **once per 24 h per user** — virtually always within the post-login window where the access token is still fresh. Workspace list is re-fetched from Nebi on every page load using the cached Bearer. - -This is the recommended fix and has not been merged yet. Skeleton: - -```python -# 03-nebi-envs.py -import time, threading, json, base64 - -_jwt_cache = {} # username -> (nebi_jwt, exp_unix) -_jwt_cache_lock = threading.Lock() - -def _cached_nebi_jwt(username): - with _jwt_cache_lock: - entry = _jwt_cache.get(username) - if entry and entry[1] > time.time() + 30: - return entry[0] - return None - -def _store_nebi_jwt(username, jwt_str): - payload = jwt_str.split(".")[1] + "=" * (4 - len(jwt_str.split(".")[1]) % 4) - exp = json.loads(base64.urlsafe_b64decode(payload)).get("exp", 0) - with _jwt_cache_lock: - _jwt_cache[username] = (jwt_str, exp) - -def get_nebi_environments(user): - username = user.get("name", "") - nebi_jwt = _cached_nebi_jwt(username) - if not nebi_jwt: - # cold path — needs fresh access token - nebi_jwt = _do_token_exchange(...) # existing logic - if nebi_jwt: - _store_nebi_jwt(username, nebi_jwt) - if not nebi_jwt: - return [] - return _list_workspaces_with_jwt(nebi_jwt, username) -``` - -Caveats: -- Nebi role / group changes for the user only take effect on next exchange (next login or 24 h). Acceptable. -- Cache lives in the japps **service** process (one of `service_workers` uvicorn workers). Each worker has its own dict — first miss in any worker triggers an exchange. Optional: shared cache via Redis or file. Not urgent. -- Cold-start path still fails if the very first env-listing call after hub restart happens >5 min after the user's last `/hub/*` hit. Mitigation: the spawner already runs the same exchange at spawn time. Hub could write the resulting JWT to a per-user file (`/tmp/nebi-jwt-`) and the env-listing callable could read that as the warm-cache source. Optional follow-up. - -## Other live work - -- **operator PR #116** — V2 attribute + audience mapper, merged-ready. The build-multiarch workflow has a temp `needs: []` stub that must be reverted before review. -- **ds-pack PR #53** — stale-token re-fetch + chart passthrough + Bearer-header reader + `forwardAccessToken: false` default. -- **hetzner-nebari main** — already pointing at the operator fix branch and the ds-pack PR branch. Revert to stable tags once both PRs land in releases. -- **nebari.openteams.ai** — local commit `15790f8` bumps the operator kustomize ref to the fix branch. Not pushed. - -## Files of interest - -- `config/jupyterhub/01-spawner.py` — `get_nebi_jwt` helper (3-step exchange), `_fetch_fresh_auth_state`, `_nebi_pre_spawn_hook`. -- `config/jupyterhub/03-nebi-envs.py` — current `get_nebi_environments` callable. Add the JWT cache here. -- `config/jupyterhub/00-gateway-auth.py` — `EnvoyOIDCAuthenticator.refresh_user`. Already reads `Authorization: Bearer` first when present. -- `templates/nebariapp.yaml` — chart passes `enforceAtGateway`, `forwardAccessToken`, `tokenExchange` through to the operator. -- `values.yaml` — `nebariapp.auth.enforceAtGateway: true`, `forwardAccessToken: false` (default).