Skip to content

Commit

Permalink
Make service positional, update poetry package to install pytest, add…
Browse files Browse the repository at this point in the history
… more tests
  • Loading branch information
elias-ba committed Dec 13, 2024
1 parent 5555685 commit 4338670
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 55 deletions.
10 changes: 3 additions & 7 deletions platform/src/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,10 @@ export const run = async (
"run",
"python",
"services/entry.py",
"--service",
scriptName,
"--input",
inputPath,
"--output",
outputPath,
"--port",
`${port}`,
...(inputPath ? ["--input", inputPath] : []),
...(outputPath ? ["--output", outputPath] : []),
...(port ? ["--port", `${port}`] : []),
],
{}
);
Expand Down
6 changes: 6 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ optional = true
[tool.poetry.group.ft.dependencies]
torch = "^2.3.0"

[tool.poetry.group.dev]
optional = false

[tool.poetry.group.dev.dependencies]
pytest = "^8.3.4"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
Expand Down
45 changes: 31 additions & 14 deletions services/entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,34 +8,44 @@
load_dotenv()


def call(*, service: str, input_path: str, output_path: str, apollo_port: int) -> dict:
def call(service: str, *, input_path: str | None = None, output_path: str | None = None, apollo_port: int | None = None) -> dict:
"""
Dynamically imports a module and invokes its main function with input data.
:param service: The name of the service/module to invoke
:param input_path: Path to the input JSON file
:param output_path: Path to write the output JSON file
:param apollo_port: Port number for Apollo server
:param input_path: Optional path to the input JSON file
:param output_path: Optional path to write the output JSON file
:param apollo_port: Optional port number for Apollo server
:return: Result from the service as a dictionary
"""
if apollo_port is not None:
set_apollo_port(apollo_port) # Set the port if provided

module_name = f"{service}.{service}"

try:
with open(input_path, "r") as f:
data = json.load(f)
except json.JSONDecodeError:
return {"type": "INTERNAL_ERROR", "code": 500, "message": "Invalid JSON input"}
data = {}
if input_path:
try:
with open(input_path, "r") as f:
data = json.load(f)
except FileNotFoundError:
return {"type": "INTERNAL_ERROR", "code": 500, "message": f"Input file not found: {input_path}"}
except json.JSONDecodeError:
return {"type": "INTERNAL_ERROR", "code": 500, "message": "Invalid JSON input"}

try:
m = __import__(module_name, fromlist=["main"])
result = m.main(data)
except ModuleNotFoundError as e:
return {"type": "INTERNAL_ERROR", "code": 500, "message": str(e)}
except ApolloError as e:
result = e.to_dict()
except Exception as e:
result = ApolloError(code=500, message=str(e), type="INTERNAL_ERROR").to_dict()

with open(output_path, "w") as f:
json.dump(result, f)
if output_path:
with open(output_path, "w") as f:
json.dump(result, f)

return result

Expand All @@ -46,8 +56,8 @@ def main():
Reads arguments from stdin and calls the appropriate service.
"""
parser = argparse.ArgumentParser(description="OpenFn Apollo Service Runner")
parser.add_argument("--service", "-s", required=True, help="Name of the service to run")
parser.add_argument("--input", "-i", required=True, help="Path to input JSON file")
parser.add_argument("service", help="Name of the service to run")
parser.add_argument("--input", "-i", help="Path to input JSON file")
parser.add_argument("--output", "-o", help="Path to output JSON file (auto-generated if not provided)")
parser.add_argument("--port", "-p", type=int, help="Apollo server port number")

Expand All @@ -65,11 +75,18 @@ def main():
print(f"Calling services/{args.service} ...")
print()

result = call(service=args.service, input_path=args.input, output_path=args.output, apollo_port=args.port)
result = call(
service=args.service,
input_path=args.input,
output_path=args.output,
apollo_port=args.port
)

print()
print("Done!")
print(result)

return result


if __name__ == "__main__":
Expand Down
131 changes: 97 additions & 34 deletions services/entry_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,55 +2,118 @@
import json
import os
from pathlib import Path
from entry import call
from entry import call, main
import argparse
from unittest.mock import patch
from util import set_apollo_port, apollo # Add apollo import

def setup_module():
"""Setup test directories"""
Path("tmp/data").mkdir(parents=True, exist_ok=True)
# Reset apollo_port to default
set_apollo_port(3000)

def test_echo_service():
# Setup test paths
def test_minimal_call():
"""Test calling with just the service name"""
result = call("echo") # Only service name required
assert result == {} # Empty dict when no input provided

def test_call_with_input():
"""Test calling with service name and input"""
input_path = "tmp/test_input.json"
output_path = "tmp/test_output.json"
test_data = {"test": "data"}

# Ensure tmp directory exists
Path("tmp").mkdir(exist_ok=True)

# Write test input
with open(input_path, "w") as f:
json.dump(test_data, f)

try:
# Call echo service with named arguments
result = call(service="echo", input_path=input_path, output_path=output_path, apollo_port=3000)
result = call("echo", input_path=input_path)
assert result == test_data

# Verify result
assert result == test_data
def test_command_line_minimal():
"""Test the absolute minimum command line usage"""
test_args = argparse.Namespace(
service="echo", # Only required argument
input=None,
output=None,
port=None
)

# Verify output file
with open(output_path, "r") as f:
output_data = json.load(f)
assert output_data == test_data
with patch('argparse.ArgumentParser.parse_args', return_value=test_args):
result = main()
assert result == {}

finally:
# Cleanup
for file in [input_path, output_path]:
if os.path.exists(file):
os.remove(file)
def test_auto_generated_output():
"""Test that output path is auto-generated when not provided"""
test_args = argparse.Namespace(
service="echo",
input=None,
output=None,
port=None
)

with patch('argparse.ArgumentParser.parse_args', return_value=test_args):
result = main()
# Check that an output file was created in tmp/data
files = list(Path("tmp/data").glob("*.json"))
assert len(files) > 0

def test_error_handling():
input_path = "tmp/test_input.json"
output_path = "tmp/test_output.json"
def test_port_setting():
"""Test that port is properly set when provided"""
test_args = argparse.Namespace(
service="echo",
input=None,
output=None,
port=5000
)

# Write invalid JSON
with open(input_path, "w") as f:
f.write("invalid json")
with patch('argparse.ArgumentParser.parse_args', return_value=test_args):
# Reset port before test
set_apollo_port(3000)
result = main()

# Instead of checking the global variable, verify the behavior
# by making a request using the apollo() function
with patch('requests.post') as mock_post:
mock_post.return_value.json.return_value = {"test": "data"}
apollo("test", {})
# Verify the URL used contains the correct port
mock_post.assert_called_with(
"http://127.0.0.1:5000/services/test",
{}
)

def test_invalid_service():
"""Test handling of invalid service name"""
result = call("nonexistent_service")
assert result["type"] == "INTERNAL_ERROR" # Check error structure instead
assert result["code"] == 500
assert "No module named" in result["message"]

def test_invalid_input_file():
"""Test handling of nonexistent input file"""
try:
result = call(service="non_existent_service", input_path=input_path, output_path=output_path, apollo_port=3000)
result = call("echo", input_path="nonexistent.json")
assert result["type"] == "INTERNAL_ERROR"
assert result["code"] == 500
finally:
# Cleanup
for file in [input_path, output_path]:
if os.path.exists(file):
os.remove(file)
except FileNotFoundError:
# Update entry.py to handle this error case
pytest.fail("FileNotFoundError should be caught and returned as error dict")

def test_output_file_writing():
"""Test that output is written to specified file"""
output_path = "tmp/test_output.json"
test_data = {"test": "data"}

result = call("echo", output_path=output_path)

assert os.path.exists(output_path)
with open(output_path, "r") as f:
written_data = json.load(f)
assert written_data == result

def teardown_module():
"""Clean up test files"""
for f in Path("tmp").glob("test_*.json"):
f.unlink(missing_ok=True)
# Reset apollo_port to default
set_apollo_port(3000)

0 comments on commit 4338670

Please sign in to comment.