nebu 0.1.27__py3-none-any.whl → 0.1.30__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
nebu/__init__.py CHANGED
@@ -3,10 +3,14 @@
3
3
  # ruff: noqa: F401
4
4
  # ruff: noqa: F403
5
5
 
6
+ from .adapter import *
7
+ from .auth import is_allowed
8
+ from .cache import Cache, OwnedValue
9
+ from .chatx.convert import *
10
+ from .chatx.openai import *
6
11
  from .config import *
7
12
  from .containers.container import Container
8
13
  from .containers.models import *
9
- from .convert import convert_to_unsloth
10
14
  from .data import *
11
15
  from .meta import *
12
16
  from .processors.decorate import *
nebu/adapter.py ADDED
@@ -0,0 +1,11 @@
1
+ import time
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+
6
+ class Adapter(BaseModel):
7
+ created_at: int = Field(default_factory=lambda: int(time.time()))
8
+ name: str
9
+ uri: str
10
+ base_model: str
11
+ owner: str
nebu/auth.py CHANGED
@@ -33,3 +33,18 @@ def get_user_profile(api_key: str) -> V1UserProfile:
33
33
  response.raise_for_status()
34
34
 
35
35
  return V1UserProfile.model_validate(response.json())
36
+
37
+
38
+ def is_allowed(
39
+ resource_owner: str,
40
+ user_id: Optional[str] = None,
41
+ orgs: Optional[Dict[str, Dict[str, str]]] = None,
42
+ ) -> bool:
43
+ if orgs is None:
44
+ orgs = {}
45
+ owners = []
46
+ for org_id, _ in orgs.items():
47
+ owners.append(org_id)
48
+ if user_id:
49
+ owners.append(user_id)
50
+ return resource_owner in owners
nebu/cache.py ADDED
@@ -0,0 +1,103 @@
1
+ import os
2
+ import time
3
+ from typing import Any, Optional, cast
4
+
5
+ import redis
6
+ from pydantic import BaseModel, Field
7
+
8
+
9
+ class OwnedValue(BaseModel):
10
+ created_at: int = Field(default_factory=lambda: int(time.time()))
11
+ value: str
12
+ user_id: Optional[str] = None
13
+ orgs: Optional[Any] = None
14
+ handle: Optional[str] = None
15
+ owner: Optional[str] = None
16
+
17
+
18
+ class Cache:
19
+ """
20
+ A simple cache class that connects to Redis.
21
+ """
22
+
23
+ def __init__(self, host: str = "localhost", port: int = 6379, db: int = 0):
24
+ """
25
+ Initializes the Redis connection.
26
+ Pulls connection details from environment variables REDIS_HOST,
27
+ REDIS_PORT, and REDIS_DB if available, otherwise uses defaults.
28
+ Also checks for REDIS_URL and prefers that if set.
29
+ """
30
+ redis_url = os.environ.get("REDIS_URL")
31
+ namespace = os.environ.get("NEBU_NAMESPACE")
32
+ if not namespace:
33
+ raise ValueError("NEBU_NAMESPACE environment variable is not set")
34
+
35
+ self.redis_client = None
36
+ connection_info = ""
37
+
38
+ try:
39
+ if redis_url:
40
+ # Use REDIS_URL if available
41
+ self.redis_client = redis.StrictRedis.from_url(
42
+ redis_url, decode_responses=True
43
+ )
44
+ connection_info = f"URL {redis_url}"
45
+ else:
46
+ # Fallback to individual host, port, db
47
+ redis_host = os.environ.get("REDIS_HOST", host)
48
+ redis_port = int(os.environ.get("REDIS_PORT", port))
49
+ redis_db = int(os.environ.get("REDIS_DB", db))
50
+ self.redis_client = redis.StrictRedis(
51
+ host=redis_host, port=redis_port, db=redis_db, decode_responses=True
52
+ )
53
+ connection_info = f"{redis_host}:{redis_port}/{redis_db}"
54
+
55
+ # Ping the server to ensure connection is established
56
+ self.redis_client.ping()
57
+ print(f"Successfully connected to Redis using {connection_info}")
58
+
59
+ self.prefix = f"cache:{namespace}"
60
+ except Exception as e:
61
+ print(f"Error connecting to Redis: {e}")
62
+ # Ensure client is None if connection fails at any point
63
+ self.redis_client = None
64
+
65
+ def get(self, key: str) -> str | None:
66
+ """
67
+ Gets the value associated with a key from Redis.
68
+ Returns None if the key does not exist or connection failed.
69
+ """
70
+ if not self.redis_client:
71
+ print("Redis client not connected.")
72
+ return None
73
+ try:
74
+ key = f"{self.prefix}:{key}"
75
+ # Cast the result to str | None as expected
76
+ result = self.redis_client.get(key)
77
+ return cast(str | None, result)
78
+ except Exception as e:
79
+ print(f"Error getting key '{key}' from Redis: {e}")
80
+ return None
81
+
82
+ def set(self, key: str, value: str, expiry_seconds: int | None = None) -> bool:
83
+ """
84
+ Sets a key-value pair in Redis.
85
+ Optionally sets an expiry time for the key in seconds.
86
+ Returns True if successful, False otherwise (e.g., connection failed).
87
+ """
88
+ if not self.redis_client:
89
+ print("Redis client not connected.")
90
+ return False
91
+ try:
92
+ key = f"{self.prefix}:{key}"
93
+ if expiry_seconds:
94
+ # Cast the result to bool
95
+ result = self.redis_client.setex(key, expiry_seconds, value)
96
+ return cast(bool, result)
97
+ else:
98
+ # Cast the result to bool
99
+ result = self.redis_client.set(key, value)
100
+ return cast(bool, result)
101
+ except Exception as e:
102
+ print(f"Error setting key '{key}' in Redis: {e}")
103
+ return False
@@ -1,34 +1,115 @@
1
1
  import base64
2
2
  import binascii
3
3
  import io
4
- from typing import Any, Dict, List
4
+ from io import BytesIO
5
+ from typing import Any, Dict, List, Tuple
5
6
 
6
7
  import requests
7
8
  from PIL import Image, UnidentifiedImageError
8
9
 
9
10
 
10
- def convert_to_unsloth(
11
- messages_input: List[Dict[str, Any]],
12
- ) -> Dict[str, List[Dict[str, Any]]]:
11
+ def convert_to_unsloth_inference(
12
+ old_schema: Dict[str, Any],
13
+ ) -> Tuple[List[Dict[str, Any]], List[Image.Image]]:
13
14
  """
14
- Converts a list of messages from an OpenAI-like chat format to the Nebulous conversation format.
15
- Images specified by URLs or base64 strings are loaded into PIL.Image objects.
16
-
17
- Input format example:
15
+ Convert from an old OpenAI message format that may look like:
18
16
  [
19
17
  {
20
18
  "role": "user",
21
19
  "content": [
22
- {"type": "input_text", "text": "Describe the image."},
23
- {"type": "input_image", "image_url": "http://... or base64 string"},
24
- ]
25
- },
20
+ {"type": "text", "text": "some text"},
21
+ {"type": "image_url", "image_url": {"url": "https://..."}},
22
+ ...
23
+ ],
24
+ }
25
+ ]
26
+
27
+ to a new format:
28
+ [
26
29
  {
27
- "role": "assistant",
28
- "content": [{"type": "text", "text": "This is an image of..."}] # Or potentially just a string
30
+ "role": "user",
31
+ "content": [
32
+ {"type": "image"},
33
+ {"type": "text", "text": "merged user text"}
34
+ ],
29
35
  }
30
36
  ]
31
37
 
38
+ Along with the new format, return a list of downloaded PIL Image objects.
39
+ """
40
+
41
+ new_schema = []
42
+ all_images = [] # Will store PIL images as we convert them
43
+
44
+ for message in old_schema:
45
+ role = message.get("role", "user")
46
+
47
+ # Collect all text pieces and all image URLs
48
+ text_chunks = []
49
+ image_urls = []
50
+
51
+ for content_item in message.get("content", []):
52
+ content_type = content_item.get("type")
53
+ if content_type == "text":
54
+ text_chunks.append(content_item.get("text", ""))
55
+ elif content_type == "image_url":
56
+ image_url = content_item.get("image_url", {}).get("url")
57
+ if image_url:
58
+ image_urls.append(image_url)
59
+
60
+ # Merge text chunks into one
61
+ merged_text = " ".join(text_chunks).strip()
62
+
63
+ # Convert each URL into a PIL image
64
+ for url in image_urls:
65
+ # Download the image
66
+ response = requests.get(url)
67
+ response.raise_for_status()
68
+ image_data = BytesIO(response.content)
69
+ pil_img = Image.open(image_data).convert("RGB")
70
+ all_images.append(pil_img)
71
+
72
+ # Construct new message format
73
+ # For simplicity, this example only places one {"type": "image"} placeholder
74
+ # regardless of how many images were found, and merges all text into one block.
75
+ new_content = []
76
+ if image_urls:
77
+ new_content.append({"type": "image"})
78
+ if merged_text:
79
+ new_content.append({"type": "text", "text": merged_text})
80
+
81
+ new_schema.append({"role": role, "content": new_content})
82
+
83
+ return new_schema, all_images
84
+
85
+
86
+ def oai_to_unsloth(
87
+ messages_input: Dict[
88
+ str, Any
89
+ ], # Assume input is always dict like {'messages': [...]}
90
+ ) -> Dict[str, List[Dict[str, Any]]]:
91
+ """
92
+ Converts messages from a JSON object containing a 'messages' key
93
+ (typical in JSON Lines format) to the Nebulous conversation format.
94
+ Images specified by URLs or base64 strings are loaded into PIL.Image objects.
95
+
96
+ Input format example (as dict from JSON line):
97
+ {
98
+ "messages": [
99
+ {
100
+ "role": "user",
101
+ "content": [
102
+ {"type": "input_text", "text": "Describe the image."},
103
+ {"type": "input_image", "image_url": "http://... or base64 string"},
104
+ ]
105
+ },
106
+ {
107
+ "role": "assistant",
108
+ "content": [{"type": "text", "text": "This is an image of..."}] # Or potentially just a string
109
+ }
110
+ ]
111
+ }
112
+
32
113
  Output format example:
33
114
  {
34
115
  "messages": [
@@ -46,8 +127,23 @@ def convert_to_unsloth(
46
127
  ]
47
128
  }
48
129
  """
130
+ # Directly extract the list of messages, assuming input structure
131
+ messages_to_process = messages_input.get("messages", [])
132
+
133
+ # Validate that 'messages' key contained a list
134
+ if not isinstance(messages_to_process, list):
135
+ print(
136
+ f"Warning: Input dict provided, but 'messages' key does not contain a list: {type(messages_to_process)}. Returning empty."
137
+ )
138
+ return {"messages": []}
139
+
49
140
  nebu_conversation = []
50
- for message in messages_input:
141
+ for message in messages_to_process: # Use the extracted list
142
+ # Add check here for robustness against malformed items *within* the list
143
+ if not isinstance(message, dict):
144
+ print(f"Warning: Skipping non-dictionary item in message list: {message!r}")
145
+ continue
146
+
51
147
  role = message.get("role")
52
148
  input_content = message.get("content") # Can be list or string
53
149