rowan-python 3.1.6__py3-none-any.whl → 3.1.7__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.
rowan/protein.py CHANGED
@@ -400,22 +400,20 @@ def upload_protein(
400
400
  if isinstance(project_uuid, Project):
401
401
  project_uuid = project_uuid.uuid
402
402
  with api_client() as client:
403
- # Step 1: Read the file and post it to the conversion endpoint.
404
403
  conversion_payload = {"name": name, "text": file_path.read_text()}
405
404
  conversion_response = client.post("/convert/pdb_file_to_protein", json=conversion_payload)
405
+ conversion_response.raise_for_status()
406
406
 
407
- # Extract the JSON data from the conversion response.
408
407
  protein_data = conversion_response.json()
409
408
 
410
- # Step 2: Use the converted data to create the final protein object.
411
409
  creation_payload = {
412
410
  "name": name,
413
411
  "protein_data": protein_data,
414
412
  "project_uuid": project_uuid,
415
413
  }
416
414
  final_response = client.post("/protein", json=creation_payload)
415
+ final_response.raise_for_status()
417
416
 
418
- # Deserialize the final JSON response into a Protein object and return it.
419
417
  return Protein(**final_response.json())
420
418
 
421
419
 
rowan/test_utils.py ADDED
@@ -0,0 +1,54 @@
1
+ """Tests for Rowan request utilities."""
2
+
3
+ import asyncio
4
+
5
+ from pytest import MonkeyPatch, raises
6
+
7
+ import rowan
8
+ from rowan.utils import api_credentials, get_api_key, get_project_uuid
9
+
10
+
11
+ def test_api_credentials_override_global_configuration(monkeypatch: MonkeyPatch) -> None:
12
+ """Prefer context-local credentials without changing global configuration."""
13
+ monkeypatch.setattr(rowan, "api_key", "global-key")
14
+ monkeypatch.setattr(rowan, "project_uuid", "global-project")
15
+ monkeypatch.setenv("ROWAN_API_KEY", "environment-key")
16
+
17
+ with api_credentials("context-key", project_uuid="context-project"):
18
+ assert get_api_key() == "context-key"
19
+ assert get_project_uuid() == "context-project"
20
+
21
+ assert get_api_key() == "global-key"
22
+ assert get_project_uuid() == "global-project"
23
+
24
+
25
+ def test_api_credentials_restore_nested_context() -> None:
26
+ """Restore outer credentials after a nested context exits."""
27
+ with api_credentials("outer-key", project_uuid="outer-project"):
28
+ with api_credentials("inner-key"):
29
+ assert get_api_key() == "inner-key"
30
+ assert get_project_uuid() is None
31
+
32
+ assert get_api_key() == "outer-key"
33
+ assert get_project_uuid() == "outer-project"
34
+
35
+
36
+ def test_api_credentials_isolate_async_tasks() -> None:
37
+ """Keep credentials isolated across concurrent asynchronous tasks."""
38
+
39
+ async def read_after_yield(api_key: str) -> str:
40
+ with api_credentials(api_key):
41
+ await asyncio.sleep(0)
42
+ return get_api_key()
43
+
44
+ async def run_workers() -> list[str]:
45
+ return list(await asyncio.gather(read_after_yield("first"), read_after_yield("second")))
46
+
47
+ assert asyncio.run(run_workers()) == ["first", "second"]
48
+
49
+
50
+ def test_api_credentials_reject_empty_key() -> None:
51
+ """Reject empty context-local API keys."""
52
+ with raises(ValueError, match="cannot be empty"):
53
+ with api_credentials(""):
54
+ pass
rowan/utils.py CHANGED
@@ -1,5 +1,7 @@
1
1
  import os
2
2
  from contextlib import contextmanager
3
+ from contextvars import ContextVar
4
+ from dataclasses import dataclass
3
5
  from typing import Generator
4
6
 
5
7
  import httpx
@@ -10,6 +12,40 @@ import rowan
10
12
  from .constants import API_URL
11
13
 
12
14
 
15
+ @dataclass(frozen=True, slots=True)
16
+ class _APIContext:
17
+ """Store credentials isolated to the current execution context."""
18
+
19
+ api_key: str
20
+ project_uuid: str | None
21
+
22
+
23
+ _api_context: ContextVar[_APIContext | None] = ContextVar("rowan_api_context", default=None)
24
+
25
+
26
+ @contextmanager
27
+ def api_credentials(api_key: str, project_uuid: str | None = None) -> Generator[None, None, None]:
28
+ """Temporarily use Rowan credentials in the current execution context.
29
+
30
+ Context-local credentials take precedence over module-level and environment configuration.
31
+ Nested contexts restore the previous credentials when they exit, and concurrent threads or
32
+ asynchronous tasks remain isolated from one another.
33
+
34
+ :param api_key: Rowan API key
35
+ :param project_uuid: active project UUID, if any
36
+ :yields: control while the credentials are active
37
+ :raises ValueError: API key is empty
38
+ """
39
+ if not api_key:
40
+ raise ValueError("API key cannot be empty.")
41
+
42
+ token = _api_context.set(_APIContext(api_key=api_key, project_uuid=project_uuid))
43
+ try:
44
+ yield
45
+ finally:
46
+ _api_context.reset(token)
47
+
48
+
13
49
  def get_api_key() -> str:
14
50
  """
15
51
  Get the API key from the environment variable ROWAN_API_KEY or the module-level attribute
@@ -19,6 +55,8 @@ def get_api_key() -> str:
19
55
 
20
56
  :returns: API key.
21
57
  """
58
+ if (context := _api_context.get()) is not None:
59
+ return context.api_key
22
60
  if hasattr(rowan, "api_key") and rowan.api_key:
23
61
  return rowan.api_key
24
62
  elif (api_key := os.environ.get("ROWAN_API_KEY")) is not None:
@@ -36,6 +74,8 @@ def get_project_uuid() -> str | None:
36
74
 
37
75
  :returns: Project UUID string, or None if not set.
38
76
  """
77
+ if (context := _api_context.get()) is not None:
78
+ return context.project_uuid
39
79
  if hasattr(rowan, "project_uuid") and rowan.project_uuid:
40
80
  return rowan.project_uuid
41
81
  return None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rowan-python
3
- Version: 3.1.6
3
+ Version: 3.1.7
4
4
  Summary: Rowan Python Library
5
5
  Project-URL: Homepage, https://github.com/rowansci/rowan-client
6
6
  Project-URL: Bug Tracker, https://github.com/rowansci/rowan-client/issues
@@ -30,9 +30,27 @@ The documentation is available [here](https://docs.rowansci.com/python-api).
30
30
 
31
31
  ## Agent skill
32
32
 
33
- Ships with a [skill](skills/computational-chemistry-and-biology/) that makes it easy for coding agents to use Rowan's tools to power chemistry and biology tasks. To use it, copy the directory into your agent's skills folder.
33
+ Ships with a [computational chemistry and biology skill](skills/computational-chemistry-and-biology/)
34
+ that helps coding agents choose and run Rowan workflows through either Rowan MCP tools or the
35
+ Rowan Python SDK.
34
36
 
35
- Download the latest skill as a zip [here](https://github.com/rowansci/rowan-python/releases/download/skill-latest/computational-chemistry-and-biology-skill.zip), then unzip it into your agent's skills folder.
37
+ ### Claude Code
38
+
39
+ ```bash
40
+ claude plugin marketplace add https://github.com/rowansci/rowan-python.git
41
+ claude plugin install computational-chemistry-and-biology@rowan
42
+ ```
43
+
44
+ ### Codex
45
+
46
+ ```bash
47
+ codex plugin marketplace add rowansci/rowan-python --ref master
48
+ codex plugin add computational-chemistry-and-biology@rowan
49
+ ```
50
+
51
+ Start a new Claude Code or Codex session after installation. For manual installation, download the
52
+ [latest skill ZIP](https://github.com/rowansci/rowan-python/releases/download/skill-latest/computational-chemistry-and-biology-skill.zip)
53
+ and extract it into your agent's skills directory.
36
54
 
37
55
  ## Running examples
38
56
 
@@ -6,11 +6,12 @@ rowan/constants.py,sha256=emCH4m9OL2Hm5E-6mJGM_FgzrK_JrZT-FiKJ6pMNQ4Y,84
6
6
  rowan/folder.py,sha256=RRg68dObluJ2RCTKX3rb7RxziOXOQUYxkfQSU2R5VMk,13708
7
7
  rowan/molecule.py,sha256=1l-3YCN_A75Uz_Q-dMWuhkeF8rhD1z2JQM7AENyjKYM,15512
8
8
  rowan/project.py,sha256=RtxYE9jv3Yz6fH5I56iDGRG5EbwlOHiSK-HF1uxc0d4,4582
9
- rowan/protein.py,sha256=T2JAqfncP80yV5gEWYA-TlpNY3WeWaNHPE0Xg5voU24,17968
9
+ rowan/protein.py,sha256=uFMF2BXgIvn6KOw3cdE-LDvZPPbjAxqqwgxakLd85XA,17763
10
10
  rowan/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ rowan/test_utils.py,sha256=DTuZqftx6ycIatajAxSGWgDaGglLvTw2TYnVJHiiNdo,1936
11
12
  rowan/types.py,sha256=MvcYvEYr6N9RXLal44MqsnMxoWfcBa-VLD_rGsW4HOc,591
12
13
  rowan/user.py,sha256=rOCYVhSoRY71fCG_OCSD7cLAWWnAMANWGitF3ogfq30,6212
13
- rowan/utils.py,sha256=DBphY1he8Vl3RNf6CbLp5BSX9RG5EfHzhMs-9TmcE_g,4299
14
+ rowan/utils.py,sha256=J-3o9_ZlOrl4_ZuuC1yjEOPdD1IkKO0BOXBncV3Ij7c,5670
14
15
  rowan/workflows/__init__.py,sha256=pkmAl8aPDznu15bSuRW0j8id-B7Xmzy_tq3LgZfQ7NQ,4786
15
16
  rowan/workflows/admet.py,sha256=jDx1Kkgmp0XaeojPzaCFY_yUwvqtSM7Cezps-93cqPw,2865
16
17
  rowan/workflows/analogue_docking.py,sha256=TshOw71RmK5XaaTIHz_Q1opiDU9x1L38tXwxoYUZaxY,9756
@@ -51,7 +52,7 @@ rowan/workflows/solvent_dependent_conformers.py,sha256=Z2xESmcM8WcTsR2TSTUa--Rrh
51
52
  rowan/workflows/spin_states.py,sha256=rjkgie2-XVNIN7O6P93yn6EPv9Ogjy8iTj3ufZIsUgY,9331
52
53
  rowan/workflows/strain.py,sha256=kCW_BlX__sdQG1JVbFZuB-57rkpnxa_JCw3h4uKdGDk,6425
53
54
  rowan/workflows/tautomer_search.py,sha256=mbRl0ZJ7wibueRF8c8_idhXJ1rtwg7LBt--5QwQ_Cck,5767
54
- rowan_python-3.1.6.dist-info/METADATA,sha256=DNhG5-OE8U_DwL4OgWgBf3tKivdvSYRMW9kmh_kq48g,2052
55
- rowan_python-3.1.6.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
56
- rowan_python-3.1.6.dist-info/licenses/LICENSE,sha256=i05z7xEhyrg6f8j0lR3XYjShnF-MJGFQ-DnpsZ8yiVI,1084
57
- rowan_python-3.1.6.dist-info/RECORD,,
55
+ rowan_python-3.1.7.dist-info/METADATA,sha256=mCUORIhOBiBBn69iu-_EXm3QUnUuv6wwDKKK84cio3o,2436
56
+ rowan_python-3.1.7.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
57
+ rowan_python-3.1.7.dist-info/licenses/LICENSE,sha256=i05z7xEhyrg6f8j0lR3XYjShnF-MJGFQ-DnpsZ8yiVI,1084
58
+ rowan_python-3.1.7.dist-info/RECORD,,