cellenium 0.1.0__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.
cellenium/__init__.py ADDED
File without changes
File without changes
File without changes
File without changes
@@ -0,0 +1,10 @@
1
+ vision:
2
+ role: >
3
+ Visual Evidence Analyst
4
+ goal: >
5
+ Extract precise, structured observations from supplied images without
6
+ inventing details that are not visibly supported.
7
+ backstory: >
8
+ You are a meticulous visual analyst. You distinguish direct observations
9
+ from uncertain interpretations, transcribe visible text exactly, and lower
10
+ confidence whenever image quality or ambiguity limits a finding.
@@ -0,0 +1,15 @@
1
+ vision_task:
2
+ description: >
3
+ Analyze every image {image} attached to this crew run with prompt: {prompt}.
4
+ Identify visible objects,
5
+ prominent colors, names or named entities, and dates. Report only evidence
6
+ supported by the image. Preserve visible spelling and date formatting,
7
+ omit duplicates, and use an empty list when a category has no findings.
8
+ Assign an overall confidence percentage that reflects image quality and
9
+ ambiguity.
10
+ expected_output: >
11
+ A VisionSchema-compatible object containing confidence_level (0 through
12
+ 100), items_found (strings), colors (objects with name and an optional
13
+ six-digit RGB hex_code), names (strings), and dates (strings). Do not
14
+ include prose or fields outside the schema.
15
+ agent: vision
@@ -0,0 +1,33 @@
1
+ from pathlib import Path
2
+ from cellenium.ai.config import AgentConfig
3
+ from crewai import Agent, Crew, Task
4
+ from cellenium.ai.agents.vision.schemas import VisionSchema
5
+ from cellenium.ai.agents.vision.tools.vision import VisionTool
6
+ from crewai.project import CrewBase, agent, crew, task
7
+
8
+
9
+ SKILLS = Path(__file__).resolve().parent / "skills"
10
+
11
+
12
+ @CrewBase
13
+ class Vision(AgentConfig):
14
+
15
+ @agent
16
+ def vision(self) -> Agent:
17
+ return Agent(config=self.agents_config["vision"], llm=self.llm, skills=[SKILLS])
18
+
19
+ @task
20
+ def vision_task(self) -> Task:
21
+ return Task(config=self.tasks_config["vision_task"], output_pydantic=VisionSchema, tools=[VisionTool(llm=self.llm)])
22
+
23
+ @crew
24
+ def crew(self) -> Crew:
25
+ return Crew(agents=self.agents, tasks=self.tasks, verbose=True)
26
+
27
+
28
+ def run_vision_agent(image: list[str], prompt: str) -> VisionSchema:
29
+ return Vision().crew().kickoff({'image': image, 'prompt': prompt}).pydantic.model_dump()
30
+
31
+
32
+ if __name__ == '__main__':
33
+ print(run_vision_agent(prompt='what is displayed?', image=[r'C:\Users\medvi\PycharmProjects\PythonProject\tests\files\images\img.png']))
@@ -0,0 +1,41 @@
1
+ from typing import Annotated
2
+ from pydantic import BaseModel, ConfigDict, Field, StringConstraints
3
+
4
+
5
+ VisionText = Annotated[str, StringConstraints(strip_whitespace=True, min_length=1, max_length=200)]
6
+
7
+
8
+ class ColorFinding(BaseModel):
9
+ """A visible color and its optional six-digit RGB value."""
10
+ model_config = ConfigDict(extra="forbid", strict=True)
11
+ name: VisionText = Field(description="Common name of the visible color.")
12
+
13
+
14
+ class VisionSchema(BaseModel):
15
+ """Structured, evidence-grounded observations from image analysis."""
16
+ model_config = ConfigDict(extra="forbid", strict=True)
17
+ confidence_level: float = Field(
18
+ ge=0,
19
+ le=100,
20
+ description="Overall confidence in the extracted findings, from 0 to 100.",
21
+ )
22
+ items_found: list[VisionText] = Field(
23
+ default_factory=list,
24
+ max_length=100,
25
+ description="Distinct visible objects or items, using short noun phrases.",
26
+ )
27
+ colors: list[ColorFinding] = Field(
28
+ default_factory=list,
29
+ max_length=50,
30
+ description="Colors visibly present in the image.",
31
+ )
32
+ names: list[VisionText] = Field(
33
+ default_factory=list,
34
+ max_length=100,
35
+ description="Names or named entities transcribed exactly as visible.",
36
+ )
37
+ dates: list[VisionText] = Field(
38
+ default_factory=list,
39
+ max_length=100,
40
+ description="Dates transcribed exactly as visible, without guessing a format.",
41
+ )
@@ -0,0 +1,14 @@
1
+ ---
2
+ name: evaluator
3
+ description: Verify proposed vision findings against image evidence before returning them.
4
+ ---
5
+
6
+ ## Evaluation Rules
7
+
8
+ 1. Re-check every proposed item, color, name, and date against the image.
9
+ 2. Remove findings that depend on assumptions, outside knowledge, or unreadable text.
10
+ 3. Keep exact transcriptions even when spelling or date formatting is unusual.
11
+ 4. Prefer omission over a plausible but unsupported guess.
12
+ 5. Confirm the confidence score reflects the least reliable material finding.
13
+ 6. Return only fields defined by the requested structured output schema.
14
+
@@ -0,0 +1,19 @@
1
+ ---
2
+ name: vision
3
+ description: Extract grounded objects, colors, visible names, and dates from attached images.
4
+ ---
5
+
6
+ ## Workflow
7
+
8
+ 1. Inspect each attached image before recording findings.
9
+ 2. Record only details directly supported by visible pixels.
10
+ 3. Use short noun phrases for objects and common names for colors.
11
+ 4. Transcribe names and dates exactly; do not normalize uncertain text.
12
+ 5. Remove duplicate findings across images.
13
+ 6. Use empty lists for categories with no visible evidence.
14
+
15
+ ## Confidence
16
+
17
+ Set `confidence_level` from 0 to 100 for the result as a whole. Reduce it for
18
+ blur, occlusion, low resolution, conflicting evidence, or uncertain text. Never
19
+ raise confidence merely to make the output appear complete.
@@ -0,0 +1,167 @@
1
+ import io
2
+ import base64
3
+ from pathlib import Path
4
+ from typing import Optional, Union, Type, List, Any
5
+ from dataclasses import dataclass
6
+ from PIL import Image
7
+ from crewai import LLM
8
+ from crewai.tools import BaseTool
9
+ from pydantic import BaseModel, Field, field_validator
10
+
11
+
12
+ # log = Logger(name="vision-tool")
13
+
14
+ MAX_IMAGE_RESOLUTION = 2048
15
+
16
+ LLMMessage = dict[str, Any]
17
+
18
+
19
+ # ------------------------------ #
20
+ # Utility Functions #
21
+ # ------------------------------ #
22
+
23
+ def is_base64(value: str) -> bool:
24
+ """Return True if value is raw base64 or a data URI, not a file path."""
25
+ return value.startswith("data:image/") or not Path(value).exists()
26
+
27
+
28
+ def resolve_path(path: str) -> str:
29
+ """Resolve and validate file path."""
30
+ resolved = Path(path).resolve()
31
+ if not resolved.exists():
32
+ # log.fire(f"Image file does not exist: {resolved}", level='debug')
33
+ raise ValueError(f"Image file not found: {resolved}")
34
+ return str(resolved)
35
+
36
+
37
+ def normalize_paths(value: Optional[Union[str, List[str]]]) -> Optional[List[str]]:
38
+ """Normalize optional image paths/base64 values to list."""
39
+ if value is None:
40
+ return None
41
+ paths = [value] if isinstance(value, str) else value
42
+ for p in paths:
43
+ if not is_base64(p):
44
+ resolve_path(p)
45
+ return paths
46
+
47
+
48
+ def _pil_to_jpeg_b64(img: "Image.Image") -> str:
49
+ if img.mode in ("RGBA", "LA", "P"):
50
+ img = img.convert("RGB")
51
+ if max(img.size) > MAX_IMAGE_RESOLUTION:
52
+ img.thumbnail((MAX_IMAGE_RESOLUTION, MAX_IMAGE_RESOLUTION), Image.LANCZOS)
53
+ buf = io.BytesIO()
54
+ img.save(buf, format="JPEG", quality=95, subsampling=0)
55
+ return f"data:image/jpeg;base64,{base64.b64encode(buf.getvalue()).decode()}"
56
+
57
+
58
+ def encode_image(path_or_b64: str) -> str:
59
+ """Encode image as base64 data URI. Accepts a file path, raw base64, or data URI."""
60
+ if path_or_b64.startswith("data:image/"):
61
+ raw = base64.b64decode(path_or_b64.split(",", 1)[1])
62
+ elif is_base64(path_or_b64):
63
+ raw = base64.b64decode(path_or_b64)
64
+ else:
65
+ try:
66
+ img = Image.open(resolve_path(path_or_b64))
67
+ return _pil_to_jpeg_b64(img)
68
+ except Exception as e:
69
+ raise ValueError(f"Cannot open image '{path_or_b64}': {e}") from e
70
+
71
+ try:
72
+ img = Image.open(io.BytesIO(raw))
73
+ return _pil_to_jpeg_b64(img)
74
+ except Exception as e:
75
+ raise ValueError(f"Failed to process image data: {e}") from e
76
+
77
+
78
+ # ------------------------------ #
79
+ # Pydantic Schema #
80
+ # ------------------------------ #
81
+
82
+ class ImagePromptSchema(BaseModel):
83
+
84
+ image_path: str = Field(..., description="Primary image path")
85
+ sample_image: Optional[Union[str, List[str]]] = Field(default=None, description="Optional comparison image path(s)")
86
+ prompt: str = Field(..., description="Prompt describing what to analyze in the image")
87
+
88
+ @classmethod
89
+ @field_validator("image_path")
90
+ def validate_image(cls, v: str) -> str:
91
+ return v if is_base64(v) else resolve_path(v)
92
+
93
+ @classmethod
94
+ @field_validator("sample_image")
95
+ def validate_sample_images(cls, v):
96
+ return normalize_paths(v)
97
+
98
+ def get_all_images(self) -> List[str]:
99
+ """Return all image paths used in the request."""
100
+ images = [self.image_path]
101
+
102
+ if self.sample_image:
103
+ images.extend(self.sample_image)
104
+
105
+ # log.fire(f"Vision images used: {images}", level='debug')
106
+
107
+ return images
108
+
109
+
110
+ # ------------------------------ #
111
+ # Tool Runtime #
112
+ # ------------------------------ #
113
+
114
+ @dataclass
115
+ class VisionMessageBuilder:
116
+
117
+ schema: ImagePromptSchema
118
+
119
+ def build(self) -> List[LLMMessage]:
120
+ images = [
121
+ {
122
+ "type": "image_url",
123
+ "image_url": {"url": encode_image(path)},
124
+ }
125
+ for path in self.schema.get_all_images()
126
+ ]
127
+
128
+ return [
129
+ {
130
+ "role": "user",
131
+ "content": [
132
+ {"type": "text", "text": self.schema.prompt},
133
+ *images
134
+ ],
135
+ }
136
+ ]
137
+
138
+
139
+ # ------------------------------ #
140
+ # Vision Tool Class #
141
+ # ------------------------------ #
142
+
143
+ class VisionTool(BaseTool):
144
+
145
+ name: str = "Bini Vision Tool"
146
+ description: str = "Analyzes one or more images using a vision-capable LLM"
147
+ args_schema: Type[BaseModel] = ImagePromptSchema
148
+
149
+ def __init__(self, llm: LLM) -> None:
150
+
151
+ super().__init__()
152
+
153
+ if llm is None:
154
+ raise ValueError("BiniVisionTool requires a valid LLM instance")
155
+
156
+ self._llm = llm
157
+
158
+ @staticmethod
159
+ def _build_messages(schema: ImagePromptSchema) -> List[LLMMessage]:
160
+ """Construct messages for the vision model."""
161
+ builder = VisionMessageBuilder(schema=schema)
162
+ return builder.build()
163
+
164
+ def _run(self, **kwargs: Any) -> str:
165
+ schema = ImagePromptSchema(**kwargs)
166
+ messages = self._build_messages(schema)
167
+ return self._llm.call(messages=messages)
cellenium/ai/config.py ADDED
@@ -0,0 +1,17 @@
1
+ from asyncio import Task
2
+ from crewai import LLM
3
+ from cellenium.settings import get_config
4
+ from functools import cached_property
5
+ from crewai.agents.agent_builder.base_agent import BaseAgent
6
+
7
+
8
+ class AgentConfig:
9
+
10
+ agents: list[BaseAgent]
11
+ tasks: list[Task]
12
+ agents_config: dict = 'config/agents.yaml'
13
+ tasks_config: dict = 'config/tasks.yaml'
14
+
15
+ @cached_property
16
+ def llm(self) -> LLM:
17
+ return LLM(model=get_config().OPENAI_MODEL)
File without changes
@@ -0,0 +1,10 @@
1
+ import base64
2
+ import mimetypes
3
+ from pathlib import Path
4
+
5
+
6
+ def encode_image_to_data_uri(image_path: str) -> str:
7
+ path = Path(image_path)
8
+ mime_type = mimetypes.guess_type(path.name)[0] or "image/png"
9
+ encoded = base64.b64encode(path.read_bytes()).decode("utf-8")
10
+ return f"data:{mime_type};base64,{encoded}"
File without changes
@@ -0,0 +1,48 @@
1
+ """
2
+ Bootstrap a Playwright browser/context/page.
3
+
4
+ Playwright ships and manages its own browser binaries (one-time setup:
5
+ `playwright install chromium`) - unlike Selenium, there is no driver-manager
6
+ download step to wire up here.
7
+ """
8
+ from typing import Optional
9
+ from dataclasses import dataclass, field
10
+ from playwright.sync_api import sync_playwright, Browser, BrowserContext, Page, Playwright
11
+ from cellenium.settings import get_config
12
+
13
+
14
+ @dataclass
15
+ class BrowserManager:
16
+
17
+ """
18
+ Launches a Playwright browser, context, and page for the lifetime of the instance.
19
+
20
+ :param headless: run without a visible UI window.
21
+ :param browser_name: one of "chromium", "firefox", "webkit".
22
+ :param http_credentials: optional {"username": ..., "password": ...} for HTTP basic
23
+ auth - Playwright only accepts this at context-creation time, so it is a
24
+ constructor field here rather than a runtime method.
25
+ """
26
+
27
+ headless: Optional[bool] = None
28
+ browser_name: str = "chromium"
29
+ http_credentials: Optional[dict] = None
30
+ playwright: Playwright = field(init=False, repr=False)
31
+ browser: Browser = field(init=False, repr=False)
32
+ context: BrowserContext = field(init=False, repr=False)
33
+ page: Page = field(init=False, repr=False)
34
+
35
+ def __post_init__(self) -> None:
36
+ if self.headless is None:
37
+ self.headless = get_config().TEST_HEADLESS
38
+ self.playwright = sync_playwright().start()
39
+ launcher = getattr(self.playwright, self.browser_name)
40
+ self.browser = launcher.launch(headless=self.headless)
41
+ self.context = self.browser.new_context(http_credentials=self.http_credentials)
42
+ self.page = self.context.new_page()
43
+
44
+ def teardown(self) -> None:
45
+ self.page.close()
46
+ self.context.close()
47
+ self.browser.close()
48
+ self.playwright.stop()
@@ -0,0 +1,79 @@
1
+ import uuid
2
+ from dataclasses import dataclass
3
+ from pathlib import Path
4
+ from typing import Literal, Optional
5
+ from playwright.sync_api import Locator
6
+ from cellenium.engine.manager import BrowserManager
7
+ from cellenium.functions.locators import get_entry, get_locator
8
+ from cellenium.functions.logger import Logger
9
+
10
+
11
+ SCREENSHOTS = Path("data") / "screenshots" # relative to the working directory, not the installed package
12
+
13
+ log = Logger(name='browser-manager')
14
+
15
+
16
+ @dataclass
17
+ class PageEngine(BrowserManager):
18
+
19
+ screen: Optional[str] = None
20
+
21
+ def get_web(self, url: str) -> None:
22
+ self.page.goto(url)
23
+ log.fire(message=f'web opened: {url} using: {self.screen} screen')
24
+
25
+ def get_element(self, name: str, timeout: int = 10_000) -> Locator:
26
+ locator = get_locator(self.page, self.screen, name)
27
+ locator.wait_for(state="attached", timeout=timeout)
28
+ log.fire(message=f'element used: {name}')
29
+ return locator
30
+
31
+ def get_dynamic_element(self, attribute: str, name: str) -> Locator:
32
+ # explanation ............ //*[contains(@<attribute>, <name>)]
33
+ # example ................ //*[contains(@name, "btnK")]
34
+ entry = get_entry(self.screen, name)
35
+ path = f"//*[contains(@{attribute}, '{entry['value']}')]"
36
+ return self.page.locator(f"xpath={path}")
37
+
38
+ def wait_for_element(self, name: str, timeout: int = 5_000) -> Locator:
39
+ locator = get_locator(self.page, self.screen, name)
40
+ locator.wait_for(state="visible", timeout=timeout)
41
+ log.fire(message=f'waiting for element: {name}')
42
+ return locator
43
+
44
+ def get_screenshot(self, name: Optional[str] = None) -> str:
45
+ SCREENSHOTS.mkdir(parents=True, exist_ok=True)
46
+ file_name = f"{name or uuid.uuid4()}.png"
47
+ path = SCREENSHOTS / file_name
48
+ self.page.screenshot(path=str(path))
49
+ return str(path)
50
+
51
+ def dropdown(self, name: str, label: str) -> None:
52
+ self.get_element(name).select_option(label=label)
53
+
54
+ def count_elements(self, name: str, tag: str) -> int:
55
+ count = self.get_element(name).locator(tag).count()
56
+ print(f"number of rows in this page is: {count}")
57
+ return count
58
+
59
+ def count_rows(self, name: str, structure: str) -> int:
60
+ return self.get_element(name).locator(structure).count()
61
+
62
+ def press_keyboard_key(self, key: str) -> None:
63
+ self.page.keyboard.press(f"Control+{key}")
64
+
65
+ def scroll_page(self, direction: Literal['up', 'down'], px: int) -> None:
66
+ match direction:
67
+ case "up":
68
+ self.page.mouse.wheel(0, -px)
69
+ case "down":
70
+ self.page.mouse.wheel(0, px)
71
+
72
+ def switch_to_new_tab(self, url: str) -> None:
73
+ new_page = self.context.new_page()
74
+ new_page.goto(url)
75
+ log.fire(message=f'url switched to {url}')
76
+
77
+ def switch_to_main_tab(self) -> None:
78
+ self.page = self.context.pages[0]
79
+ self.page.bring_to_front()
File without changes
@@ -0,0 +1,38 @@
1
+ from collections.abc import Callable
2
+
3
+ from playwright.sync_api import Locator, Page
4
+
5
+ from cellenium.functions.sheets import fetch_locators
6
+
7
+ # Selenium's find_element silently returns the first match; Playwright locators are
8
+ # strict-mode by default and raise on multiple matches, so each strategy here resolves
9
+ # to the first match (via .first) to keep that same "resolve to one element" semantics.
10
+ LOCATOR_MAP: dict[str, Callable[[Page, str], Locator]] = {
11
+ "NAME": lambda page, v: page.locator(f'[name="{v}"]').first,
12
+ "XPATH": lambda page, v: page.locator(f"xpath={v}").first,
13
+ "ID": lambda page, v: page.locator(f"#{v}").first,
14
+ "CSS": lambda page, v: page.locator(v).first,
15
+ "CLASS_NAME": lambda page, v: page.locator(f".{v}").first,
16
+ "LINK_TEXT": lambda page, v: page.get_by_text(v, exact=True).first,
17
+ "TAG_NAME": lambda page, v: page.locator(v).first,
18
+ }
19
+
20
+
21
+ def get_entry(screen: str | None, name: str) -> dict[str, str]:
22
+ """Look up a raw POM entry from the worksheet selected by screen."""
23
+ entries = fetch_locators(screen)
24
+ if name not in entries:
25
+ raise KeyError(f"No locator named {name!r} on screen {screen!r}")
26
+
27
+ return entries[name]
28
+
29
+
30
+ def get_locator(page: Page, screen: str | None, name: str) -> Locator:
31
+ """Resolve a Google Sheets POM entry to a Playwright Locator."""
32
+ entry = get_entry(screen, name)
33
+ strategy = entry["type"]
34
+ try:
35
+ builder = LOCATOR_MAP[strategy]
36
+ except KeyError:
37
+ raise ValueError(f"Unsupported locator strategy {strategy!r} for {name!r} on screen {screen!r}")
38
+ return builder(page, entry["value"])
@@ -0,0 +1,44 @@
1
+ from dataclasses import dataclass
2
+ from functools import cached_property
3
+ from typing import Literal
4
+
5
+ import logfire
6
+
7
+ from cellenium.settings import get_config
8
+
9
+ LogLevel = Literal["info", "error", "debug", "warning", "fatal"]
10
+
11
+
12
+ @dataclass
13
+ class Logger:
14
+
15
+ name: str
16
+
17
+ @cached_property
18
+ def config(self):
19
+ # Configured on first log call so module-level loggers don't load settings at import time.
20
+ return logfire.configure(
21
+ token=get_config().LOGFIRE_TOKEN,
22
+ send_to_logfire="if-token-present",
23
+ )
24
+
25
+ def fire(self, message: str, level: LogLevel | None = 'info', **attributes: object) -> None:
26
+ with self.config.span(self.name):
27
+ match level:
28
+ case "info":
29
+ self.config.info(message, **attributes)
30
+ case "error":
31
+ self.config.error(message, **attributes)
32
+ case "debug":
33
+ self.config.debug(message, **attributes)
34
+ case "warning":
35
+ self.config.warning(message, **attributes)
36
+ case "fatal":
37
+ self.config.fatal(message, **attributes)
38
+ case _:
39
+ self.config.info(message, **attributes)
40
+
41
+
42
+ if __name__ == "__main__":
43
+ logger = Logger(name="app")
44
+ logger.fire(message="hi")
@@ -0,0 +1,71 @@
1
+ """
2
+ Read Page Object Model locators directly from Google Sheets.
3
+ Uses gspread with a service-account credentials.json (no "anyone with the
4
+ link" sharing required) - the sheet just needs to be shared with the service
5
+ account's client_email (see credentials.json) as a Viewer.
6
+
7
+ Each worksheet (tab) in the spreadsheet is one screen. Callers select the
8
+ worksheet by passing the same screen name used by ``PageEngine``.
9
+
10
+ Sheet columns (per the QA team's POM sheet):
11
+ name - identifier used to look up the locator in code
12
+ locator - the strategy, e.g. NAME / XPATH / ID / CSS
13
+ type - the actual locator value (confusingly named, but matches the sheet)
14
+ actions - optional
15
+ comments - optional
16
+ """
17
+
18
+ import gspread
19
+ from pathlib import Path
20
+ from typing import Optional
21
+ from cellenium.functions.logger import Logger
22
+ from cellenium.settings import get_config
23
+
24
+
25
+ log = Logger('sheets-logic')
26
+
27
+
28
+ def _parse_worksheet(worksheet: gspread.Worksheet) -> dict[str, dict[str, str]]:
29
+
30
+ rows = worksheet.get_all_values()
31
+
32
+ if not rows:
33
+ log.fire(message='no rows were found', level='debug')
34
+ return {}
35
+
36
+ header = [h.strip().lower() for h in rows[0]]
37
+ locators: dict[str, dict[str, str]] = {}
38
+
39
+ for row in rows[1:]:
40
+ record = dict(zip(header, row))
41
+ name = record.get("name", "").strip()
42
+ if not name:
43
+ continue
44
+ locators[name] = {
45
+ "type": record.get("locator", "").strip().upper(),
46
+ "value": record.get("type", "").strip(),
47
+ "actions": record.get("actions", "").strip(),
48
+ "comments": record.get("comments", "").strip(),
49
+ }
50
+ return locators
51
+
52
+
53
+ def fetch_locators(screen: Optional[str] = None, credentials_path: Optional[Path] = None) -> dict[str, dict]:
54
+ """Read all locator rows from one worksheet selected by screen name."""
55
+ if not screen:
56
+ message = 'screen is required to select a Google Sheets worksheet'
57
+ log.fire(message=message, level='error')
58
+ raise ValueError(message)
59
+
60
+ config = get_config()
61
+ gc = gspread.service_account(filename=str(credentials_path or config.CREDENTIALS_JSON))
62
+ spreadsheet = gc.open_by_url(config.GOOGLE_SHEETS)
63
+
64
+ try:
65
+ worksheet = spreadsheet.worksheet(screen)
66
+
67
+ except gspread.WorksheetNotFound as e:
68
+ log.fire(f"No worksheet named {screen!r} in the configured Google Sheet", level='error')
69
+ raise ValueError from e
70
+
71
+ return _parse_worksheet(worksheet)
cellenium/settings.py ADDED
@@ -0,0 +1,26 @@
1
+ from functools import cache
2
+ from pathlib import Path
3
+ from pydantic import Field
4
+ from pydantic_settings import BaseSettings, SettingsConfigDict
5
+
6
+
7
+ class Settings(BaseSettings):
8
+
9
+ # Relative paths resolve against the current working directory, i.e. the project using cellenium.
10
+ model_config = SettingsConfigDict(env_file=".env", extra="allow")
11
+
12
+ GOOGLE_SHEETS: str = Field(..., description='')
13
+ GOOGLE_SHEET_API_KEY: str = Field(..., description='')
14
+ GOOGLE_SHEET_EMAIL: str = Field(..., description='')
15
+ GOOGLE_SHEET_ID: str = Field(..., description='')
16
+ OPENAI_MODEL: str = Field(..., description='')
17
+ OPENAI_API_KEY: str | None = Field(None, description='')
18
+ LOGFIRE_TOKEN: str | None = Field(None, description='')
19
+ TEST_HEADLESS: bool = Field(True, description='')
20
+ CREDENTIALS_JSON: Path = Field(Path("credentials.json"), description='google json credentials for accessing the sheet')
21
+
22
+
23
+ @cache
24
+ def get_config() -> Settings:
25
+ """Load settings on first use so importing cellenium never requires a configured environment."""
26
+ return Settings()
@@ -0,0 +1,224 @@
1
+ Metadata-Version: 2.5
2
+ Name: cellenium
3
+ Version: 0.1.0
4
+ Summary: Playwright browser automation with a Google Sheets Page Object Model and CrewAI vision analysis
5
+ Project-URL: Homepage, https://github.com/MedviJenka/cellenium-playwright
6
+ License-File: LICENSE
7
+ Requires-Python: >=3.13
8
+ Requires-Dist: crewai>=1.15.23
9
+ Requires-Dist: fastapi>=0.142.2
10
+ Requires-Dist: gspread>=6.2.1
11
+ Requires-Dist: importlib-metadata>=8.0.0
12
+ Requires-Dist: logfire>=3.5.1
13
+ Requires-Dist: oauth2client>=4.1.3
14
+ Requires-Dist: pillow>=12.3.0
15
+ Requires-Dist: playwright>=1.63.0
16
+ Requires-Dist: pydantic-settings>=2.15.0
17
+ Requires-Dist: uvicorn>=0.54.0
18
+ Description-Content-Type: text/markdown
19
+
20
+ # Cellenium Playwright
21
+
22
+ Cellenium Playwright is an experimental Python browser-automation toolkit built on [Playwright](https://playwright.dev/python/). It provides browser lifecycle management, a Google-Sheets-backed Page Object Model (POM), screenshot capture, and components for structured visual analysis with CrewAI.
23
+
24
+ The repository also contains **TestFlow**, a set of Claude Code skills that turns product requirements and technical specifications into traceable pytest tests, validated UI locators, execution evidence, and a test-quality verdict.
25
+
26
+ > **Status:** active development. The browser and locator layers are usable, but the checked-in vision test is still a scaffold and does not currently pass. Install it from PyPI with `pip install cellenium` (or `uv add cellenium`), then run `playwright install chromium`. Configuration (`.env`, `credentials.json`) is read from your project's working directory.
27
+
28
+ ## Features
29
+
30
+ - Synchronous Chromium, Firefox, or WebKit sessions through Playwright.
31
+ - Reusable `PageEngine` helpers for navigation, locators, screenshots, scrolling, dropdowns, and tabs.
32
+ - Page Object Model data read directly from Google Sheets at runtime.
33
+ - Locator strategies for name, XPath, ID, CSS, class name, link text, and tag name.
34
+ - Structured image-analysis components built with CrewAI and Pydantic.
35
+ - Parallel pytest execution in CI on Python 3.13 and 3.14.
36
+ - Optional TestFlow skills for end-to-end pytest test generation and evaluation.
37
+
38
+ ## Requirements
39
+
40
+ - Python 3.13 or 3.14
41
+ - [`uv`](https://docs.astral.sh/uv/)
42
+ - A Playwright-supported browser
43
+ - A Google service account for resolving locators
44
+ - An OpenAI-compatible model and API key when using the vision components
45
+
46
+ ## Quick start
47
+
48
+ Clone the repository and install the locked dependencies:
49
+
50
+ ```sh
51
+ git clone https://github.com/MedviJenka/cellenium-playwright.git
52
+ cd cellenium-playwright
53
+ uv sync --frozen
54
+ ```
55
+
56
+ Install Chromium:
57
+
58
+ ```sh
59
+ uv run playwright install chromium
60
+ ```
61
+
62
+ On Ubuntu or another Debian-based CI runner, install Chromium and its system dependencies together:
63
+
64
+ ```sh
65
+ uv run playwright install --with-deps chromium
66
+ ```
67
+
68
+ ## Configuration
69
+
70
+ Create `.env` in the repository root. Optional values are labeled below:
71
+
72
+ ```dotenv
73
+ GOOGLE_SHEETS=<spreadsheet-id-or-edit-url>
74
+ GOOGLE_SHEET_API_KEY=<google-api-key>
75
+ GOOGLE_SHEET_EMAIL=<service-account-email>
76
+ GOOGLE_SHEET_ID=<spreadsheet-id>
77
+ OPENAI_MODEL=<model-name>
78
+ OPENAI_API_KEY=<optional-api-key>
79
+ LOGFIRE_TOKEN=<optional-logfire-token>
80
+ TEST_HEADLESS=true
81
+ ```
82
+
83
+ `GOOGLE_SHEETS` selects the locator spreadsheet. When `LOGFIRE_TOKEN` is omitted, logging remains local and no data is sent to Logfire.
84
+
85
+ Place the Google service-account document at:
86
+
87
+ ```text
88
+ credentials.json
89
+ ```
90
+
91
+ Share the locator spreadsheet with the service account's `client_email` as a viewer. Never commit `.env` or `credentials.json`; both are ignored by Git.
92
+
93
+ ## Google Sheets Page Object Model
94
+
95
+ Each worksheet tab represents one screen. The first row must contain these columns:
96
+
97
+ | Column | Purpose |
98
+ |---|---|
99
+ | `name` | Logical element name used by tests |
100
+ | `locator` | Locator strategy such as `NAME`, `XPATH`, `ID`, or `CSS` |
101
+ | `type` | Locator value; this name is retained for compatibility with the source sheet |
102
+ | `actions` | Optional metadata |
103
+ | `comments` | Optional notes |
104
+
105
+ Example worksheet:
106
+
107
+ | name | locator | type | actions | comments |
108
+ |---|---|---|---|---|
109
+ | search | NAME | q | fill | Google search field |
110
+ | button | NAME | btnK | click | Search button |
111
+
112
+ `PageEngine(screen="Google")` reads locator rows directly from the worksheet tab
113
+ named `Google`. No generated JSON mapping or synchronization step is required.
114
+
115
+ ## Browser automation
116
+
117
+ ```python
118
+ from src.core.engine.page_engine import PageEngine
119
+
120
+ engine = PageEngine(screen="Google", headless=True)
121
+
122
+ try:
123
+ engine.get_web("https://www.google.com")
124
+ engine.get_element("search").fill("cats")
125
+ engine.get_element("button").click()
126
+ screenshot = engine.get_screenshot("google-results")
127
+ print(screenshot)
128
+ finally:
129
+ engine.teardown()
130
+ ```
131
+
132
+ `screen="Google"` selects the worksheet tab named `Google`. With multiple tabs, pass the page's tab name when constructing `PageEngine` (for example, `Google`, `Heroku`, or `ST`). Missing worksheets, missing element names, and unsupported locator strategies raise explicit exceptions.
133
+
134
+ ## Tests and linting
135
+
136
+ Run the test suite:
137
+
138
+ ```sh
139
+ uv run --frozen pytest src/tests tests -v -n auto --dist loadscope
140
+ ```
141
+
142
+ `src/tests/test_google_search.py` exercises the live browser flow. The unit regressions in `tests/` cover screenshot persistence and direct worksheet selection without contacting external services.
143
+
144
+ Run the blocking CI lint checks:
145
+
146
+ ```sh
147
+ uvx --from flake8==7.4.1 flake8 main.py settings.py src \
148
+ --count --select=E9,F63,F7,F82 --show-source --statistics
149
+ ```
150
+
151
+ The GitHub Actions workflow in `.github/workflows/python-package.yml` performs locked dependency installation, installs Chromium, runs lint checks, and executes the tests under Xvfb for Python 3.13 and 3.14.
152
+
153
+ ## TestFlow Claude Code skills
154
+
155
+ TestFlow separates pytest creation into focused stages:
156
+
157
+ ```text
158
+ PRD → test specification → UI locators → pytest writer → executor → evaluator
159
+ ```
160
+
161
+ | Order | Skill | Purpose |
162
+ |---:|---|---|
163
+ | Optional | `web` | Explore a live application in a real browser and produce reproducible bug evidence |
164
+ | 1 | `prd-reader` | Extract explicit, derived, and unknown test requirements from the PRD |
165
+ | 2 | `test-spec` | Design traceable pytest cases, boundaries, fixtures, and execution prerequisites |
166
+ | 3 | `locator-writer` | For UI cases, validate stable Playwright selectors and upsert elements into Google Sheets |
167
+ | 4 | `writer` | Implement deterministic, behavior-focused pytest tests |
168
+ | 5 | `executor` | Run collection, targeted tests, related tests, and classify failures |
169
+ | 6 | `evaluator` | Evaluate traceability, regression sensitivity, coverage quality, and execution evidence |
170
+
171
+ `artifact` orchestrates the complete pipeline and repeats only the affected downstream stages when evaluation finds a defect.
172
+
173
+ `web` is a report-only exploratory entry point. Verified findings become regression candidates for `test-spec`; it does not modify application code.
174
+
175
+ Validate the Claude Code plugin metadata:
176
+
177
+ ```sh
178
+ claude plugin validate .
179
+ ```
180
+
181
+ Run Claude Code from the repository and invoke either the complete pipeline or an individual stage:
182
+
183
+ ```text
184
+ /testflow:web
185
+ /testflow:artifact
186
+ /testflow:prd-reader
187
+ /testflow:test-spec
188
+ /testflow:locator-writer
189
+ /testflow:writer
190
+ /testflow:executor
191
+ /testflow:evaluator
192
+ ```
193
+
194
+ The skills are manual-only. The locator writer previews changes before replacing existing Google Sheet selectors and verifies selectors against the live UI.
195
+
196
+ The separate checklist skills live under `.claude/skills/checklist/`: `before-startup` (system ready to build tests), `cruising` (everything in place, critical errors and potential bugs), `landing` (final production readiness), and the standalone `fordec` emergency decision skill.
197
+
198
+ ## Project layout
199
+
200
+ ```text
201
+ .github/workflows/ GitHub Actions CI
202
+ .claude/skills/testflow/ Pytest TestFlow skill definitions
203
+ .claude/skills/checklist/ Checklist skills (before-startup, cruising, landing, fordec)
204
+ .claude/commands/testflow/ TestFlow slash-command entries
205
+ .claude-plugin/plugin.json Claude Code plugin metadata
206
+ src/core/engine/ Playwright browser and page engines
207
+ src/core/functions/ Locator resolution and direct Google Sheets access
208
+ src/core/ai/ CrewAI configuration and vision components
209
+ src/tests/ Browser test scenarios
210
+ settings.py Environment-backed runtime configuration
211
+ pyproject.toml Python requirements and dependencies
212
+ uv.lock Reproducible dependency lockfile
213
+ ```
214
+
215
+ ## Known limitations
216
+
217
+ - The checked-in vision test uses a placeholder `VisionAssertion` without a `run()` implementation.
218
+ - The sample end-to-end test depends on Google and is not hermetic.
219
+ - Configuration currently requires fields that are reserved but not consumed by every component.
220
+ - The project does not yet define a wheel/sdist build or publishing workflow.
221
+
222
+ ## License
223
+
224
+ Licensed under the [ISC License](LICENSE). Copyright © 2026 Jenia Petrusenko.
@@ -0,0 +1,26 @@
1
+ cellenium/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ cellenium/settings.py,sha256=6r5flnKTZ0Sr16Y_SIuPXQEGr-bWtR9N6UirColrEiQ,1384
3
+ cellenium/ai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ cellenium/ai/config.py,sha256=xpT5HDa0A1ff_H-UyZAVWHb_t2WhLJG9Ysx5NVpTTbk,470
5
+ cellenium/ai/agents/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ cellenium/ai/agents/vision/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ cellenium/ai/agents/vision/crew.py,sha256=-LKXwVYVQjCrnhbfVoTx39l1P6oDcAPhLsA2Xb0d6Ps,1158
8
+ cellenium/ai/agents/vision/schemas.py,sha256=KZPZYlPzC95GPyEfz-qNYpC-HMLs8E0INIoKzf70ixY,1505
9
+ cellenium/ai/agents/vision/config/agents.yaml,sha256=ukcIjykZ8_M8NDQwsByUgU2L7UPdHK23x3c2YhzGMLU,434
10
+ cellenium/ai/agents/vision/config/tasks.yaml,sha256=oV4aA1WxgjEzeOwvI73mqCM5XmlV_stZ9Li5vi2S2S8,788
11
+ cellenium/ai/agents/vision/skills/evaluator/SKILL.md,sha256=icUO8l-eum2qAkb7hXnRJ817BDedv3WMXMHRPMpvgVs,601
12
+ cellenium/ai/agents/vision/skills/vision/SKILL.md,sha256=m75iOqKpqAguUolohZ9cKnifzqElej6NVdyvgvYLl2M,744
13
+ cellenium/ai/agents/vision/tools/vision.py,sha256=fx9Pfwh-VmItyAcdiAum_9ROWH8mlDwgJ9WT68qd9xg,5319
14
+ cellenium/ai/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ cellenium/ai/tools/image_encoder.py,sha256=b4M5W_FwJ2iH8ZwS4AmCBbXXpZ-fM6YlYkapzHDLUoY,330
16
+ cellenium/engine/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
+ cellenium/engine/manager.py,sha256=Fi7lTVXtkI8Lht-tAJuYr1gfraEEBo2ExmgIpfi2-fg,1892
18
+ cellenium/engine/page_engine.py,sha256=iwaDj9R5IjZym86nz9uh9yONqAj8h2WIocQ2gQVXrM0,3032
19
+ cellenium/functions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
+ cellenium/functions/locators.py,sha256=hoCfO8zg9dljeK1squKyt-sH5dKjZaRC0jtKZcivkBs,1648
21
+ cellenium/functions/logger.py,sha256=68uZtamrCtCqmHMSwcXcDUPiz4HkjhXz0rbEYanFzU0,1344
22
+ cellenium/functions/sheets.py,sha256=UYQUmZYrTOZlWN1oapzLwOnQ1ZxQAHiTu24ggCF1UL0,2545
23
+ cellenium-0.1.0.dist-info/METADATA,sha256=NN_aezscdzaya0FhxMjWVRKJ9dISAXHF7jeM4bdtaKk,8990
24
+ cellenium-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
25
+ cellenium-0.1.0.dist-info/licenses/LICENSE,sha256=TsSLLu43bQqPdjqazLS4p4OpWvYomR6MYlLs10is-P4,754
26
+ cellenium-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,7 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026 Jenia Petrusenko
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.