Skip to content

Commit

Permalink
feat(python): support cloud.Api (#231)
Browse files Browse the repository at this point in the history
  • Loading branch information
eladcon authored May 21, 2024
1 parent 43c3a11 commit b680f38
Show file tree
Hide file tree
Showing 13 changed files with 252 additions and 11 deletions.
24 changes: 24 additions & 0 deletions python/api.test.w
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
bring cloud;
bring http;
bring expect;
bring util;
bring "./lib.w" as python;

let bucket = new cloud.Bucket();
let api = new cloud.Api();
api.get("/test", new python.InflightApiEndpointHandler(
path: "./test-assets",
handler: "main.api_handler",
).lift(bucket, id: "bucket", allow: ["put"]));

test "invokes api handler" {
let res = http.get("{api.url}/test");
log(Json.stringify(res));
expect.equal(res.status, 200);
expect.equal(res.body, "Hello from Api Handler!");
expect.equal(res.headers["header1"], "value1");

util.waitUntil(inflight () => {
return bucket.exists("/test");
});
}
29 changes: 29 additions & 0 deletions python/lib.w
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ bring util;
bring "constructs" as construct;
bring "./types.w" as types;
bring "./sim" as sim;
bring "./sim/api_onrequest_inflight.w" as simapi;
bring "./tfaws/inflight.w" as aws;
bring "./tfaws/api_onrequest_inflight.w" as tfawsapi;

pub class InflightFunction impl cloud.IFunctionHandler {
_inflightType: str;
Expand Down Expand Up @@ -112,3 +114,30 @@ pub class InflightBucketEvent impl cloud.IBucketEventHandler {
return this;
}
}

pub class InflightApiEndpointHandler impl cloud.IApiEndpointHandler {
_inflightType: str;
inner: types.IApiOnRequest;

new(props: types.InflightProps) {
this._inflightType = "_inflightPython";

let target = util.env("WING_TARGET");
if target == "sim" {
this.inner = new simapi.InflightApiEndpointHandler(props);
} elif target == "tf-aws" {
this.inner = new tfawsapi.Inflight_tfaws(props);
} else {
throw "Unsupported target ${target}";
}
}

pub inflight handle(req: cloud.ApiRequest): cloud.ApiResponse? {
return this.inner.handle(req);
}

pub lift(obj: std.Resource, options: types.LiftOptions): InflightApiEndpointHandler {
this.inner.lift(obj, options);
return this;
}
}
4 changes: 2 additions & 2 deletions python/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion python/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@winglibs/python",
"description": "python library for Wing",
"version": "0.0.2",
"version": "0.0.3",
"repository": {
"type": "git",
"url": "https://github.com/winglang/winglibs.git",
Expand Down
26 changes: 26 additions & 0 deletions python/sim/api_onrequest_inflight.w
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
bring cloud;
bring "./inflight.w" as inflyght;
bring "../types.w" as types;

pub class InflightApiEndpointHandler extends inflyght.Inflight impl cloud.IApiEndpointHandler {
new(props: types.InflightProps) {
super(props);
}

pub inflight handle(req: cloud.ApiRequest): cloud.ApiResponse? {
let response = this._handle(Json.stringify(req));
if let res = Json.tryParse(response) {
let headers = MutMap<str>{};
for entry in Json.entries(res["headers"]) {
headers.set(entry.key, entry.value.asStr());
}
return {
status: res["statusCode"].asNum(),
headers: headers.copy(),
body: res["body"].tryAsStr(),
};
} else {
return nil;
}
}
}
16 changes: 16 additions & 0 deletions python/test-assets/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,19 @@ def bucket_oncreate_handler(event, context):
"statusCode": 200,
"body": "Hello from Bucket OnCreate!"
}

def api_handler(event, context):
print(event)
print(context)

req = from_api_event(event)
client_put = lifted("bucket")
client_put.put(req.path, req.toJSON())

return from_api_response({
"status": 200,
"body": "Hello from Api Handler!",
"headers": {
"header1": "value1"
}
})
65 changes: 65 additions & 0 deletions python/test-assets/wing.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,68 @@ def from_bucket_event(event):
return [bucket_event]
else:
raise Exception(f"Unsupported target: {target}")

class ApiRequest:
method: str
path: str
query: dict
headers: dict
body: str
vars: dict

def toJSON(self):
return json.dumps(
self,
default=lambda o: o.__dict__,
sort_keys=True,
indent=2)

def from_api_event(event):
target = os.getenv(f"WING_TARGET")
if target == "tf-aws":
req = ApiRequest()
req.method = event["httpMethod"]
req.path = event["path"]
req.query = event["queryStringParameters"]
req.headers = event["headers"]
req.body = event["body"]
req.vars = event["pathParameters"]
return req
elif target == "sim":
data = event["payload"]
req = ApiRequest()
req.method = data["method"]
req.path = data["path"]
req.query = data["query"]
req.headers = data["headers"]
req.body = data["body"]
req.vars = data["vars"]
return req
else:
raise Exception(f"Unsupported target: {target}")

def from_api_response(res = None):
if not res:
return {
"statusCode": 200,
"body": "",
"headers": {}
}

response = {}
if not res.get("status"):
response["statusCode"] = 200
else:
response["statusCode"] = res["status"]

if not res.get("body"):
response["body"] = ""
else:
response["body"] = res["body"]

if not res.get("headers"):
response["headers"] = {}
else:
response["headers"] = res["headers"]

return response
27 changes: 27 additions & 0 deletions python/tfaws/api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const { Api: TfAwsApi } = require("@winglang/sdk/lib/target-tf-aws/api.js");
const { App } = require("@winglang/sdk/lib/target-tf-aws/app.js");
const { Node } = require("@winglang/sdk/lib/std/node.js");
const awsProvider = require("@cdktf/provider-aws");
const { Function } = require("./function.js");

module.exports.Api = class Api extends TfAwsApi {
addHandler(inflight, method, path) {
if (inflight._inflightType !== "_inflightPython") {
return super.addHandler(inflight, method, path);
}

let handler = this.handlers[inflight._id];
if (!handler) {
handler = new Function(
this,
App.of(this).makeId(this, `${this.node.id}-OnMessage`),
inflight,
{},
);
Node.of(handler).hidden = true;
this.handlers[inflight._id] = handler;
}

return handler;
}
};
19 changes: 19 additions & 0 deletions python/tfaws/api_onrequest_inflight.w
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
bring cloud;
bring util;
bring "../types.w" as types;
bring "./inflight.w" as inflyght;

pub class Inflight_tfaws extends inflyght.Inflight_tfaws impl types.IApiOnRequest {
new(props: types.InflightProps) {
super(props);
}

pub inflight handle(req: cloud.ApiRequest): cloud.ApiResponse? {

}

pub lift(obj: std.Resource, options: types.LiftOptions): cloud.IApiEndpointHandler {
this.lifts.set(options.id, { client: obj, options: options });
return this;
}
}
39 changes: 32 additions & 7 deletions python/tfaws/function.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
const { Construct } = require("constructs");
const { Function: TfAwsFunction } = require("@winglang/sdk/lib/target-tf-aws/function.js");
const { App } = require("@winglang/sdk/lib/target-tf-aws/app.js");
const { Testing } = require("@winglang/sdk/lib/simulator");
const { inflight: createInflight } = require("@winglang/sdk/lib/core");
const { Function: AwsFunction } = require("@winglang/sdk/lib/shared-aws/function.js");
const { Bucket: AwsBucket } = require("@winglang/sdk/lib/shared-aws/bucket.js");
const { normalPath } = require("@winglang/sdk/lib/shared/misc.js");
const { Duration } = require("@winglang/sdk/lib/std/duration.js");
const { ResourceNames } = require("@winglang/sdk/lib/shared/resource-names");
const { DEFAULT_MEMORY_SIZE } = require("@winglang/sdk/lib/shared/function");
const cdktf = require("cdktf");
const awsProvider = require("@cdktf/provider-aws");
const { build } = require("../util.js");

const FUNCTION_NAME_OPTS = {
maxLen: 64,
disallowedRegex: /[^a-zA-Z0-9\_\-]+/g,
};

module.exports.Function = class Function extends Construct {
constructor(
scope,
Expand All @@ -18,11 +25,8 @@ module.exports.Function = class Function extends Construct {
props = {},
) {
super(scope, id);
this.dummy = new TfAwsFunction(this, "Dummy", Testing.makeHandler(`
async handle(event) {
return;
}`
));

this.dummy = new TfAwsFunction(this, "Dummy", createInflight(async (ctx) => {}));
const entrypointDir = App.of(this).entrypointDir;
const workDir = App.of(this).workdir;
const pathEnv = process.env["PATH"] || "";
Expand All @@ -42,6 +46,7 @@ module.exports.Function = class Function extends Construct {
});

const roleArn = this.dummy.role.arn;
const roleName = this.dummy.role.name;

const clients = {};
for (let clientId of Object.keys(inflight.inner.lifts)) {
Expand All @@ -63,8 +68,27 @@ module.exports.Function = class Function extends Construct {
WING_CLIENTS: cdktf.Fn.jsonencode(clients),
};

this.name = ResourceNames.generateName(this, FUNCTION_NAME_OPTS);
this.functionName = this.name;

// Add execution role for lambda to write to CloudWatch logs
new awsProvider.iamRolePolicyAttachment.IamRolePolicyAttachment(this, "IamRolePolicyAttachment", {
policyArn:
"arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole",
role: roleName,
});

if (!props.logRetentionDays || props.logRetentionDays >= 0) {
new awsProvider.cloudwatchLogGroup.CloudwatchLogGroup(this, "CloudwatchLogGroup", {
name: `/aws/lambda/${this.name}`,
retentionInDays: props.logRetentionDays ?? 30,
});
} else {
// Negative value means Infinite retention
}

this.lambda = new awsProvider.lambdaFunction.LambdaFunction(this, "PyFunction", {
functionName: `${id}-${this.node.addr.substring(42-8)}`,
functionName: this.name,
role: roleArn,
handler: inflight.inner.props.handler,
runtime: "python3.11",
Expand All @@ -73,6 +97,7 @@ module.exports.Function = class Function extends Construct {
timeout: props.timeout
? props.timeout.seconds
: Duration.fromMinutes(1).seconds,
memorySize: props.memory ?? DEFAULT_MEMORY_SIZE,
environment: {
variables: cdktf.Lazy.anyValue({
produce: () => ({
Expand Down
2 changes: 1 addition & 1 deletion python/tfaws/inflight.w
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ struct Lifted {

pub class Inflight_tfaws impl types.IInflight {
props: types.InflightProps;
lifts: MutMap<Lifted>;
protected lifts: MutMap<Lifted>;

new(props: types.InflightProps) {
this.props = props;
Expand Down
4 changes: 4 additions & 0 deletions python/types.w
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,7 @@ pub interface IInflight extends cloud.IFunctionHandler {
pub interface IBucketEventInflight extends cloud.IBucketEventHandler {
lift(obj: std.Resource, options: LiftOptions): cloud.IBucketEventHandler;
}

pub interface IApiOnRequest extends cloud.IApiEndpointHandler {
lift(obj: std.Resource, options: LiftOptions): cloud.IApiEndpointHandler;
}
6 changes: 6 additions & 0 deletions python/wplatform.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ const { Function: TfAwsFunction } = require("./tfaws/function.js");
const { Queue: TfAwsQueue } = require("./tfaws/queue.js");
const { Topic: TfAwsTopic } = require("./tfaws/topic.js");
const { Bucket: TfAwsBucket } = require("./tfaws/bucket.js");
const { Api: TfAwsApi } = require("./tfaws/api.js");

const FUNCTION_FQN = "@winglang/sdk.cloud.Function";
const QUEUE_FQN = "@winglang/sdk.cloud.Queue";
const TOPIC_FQN = "@winglang/sdk.cloud.Topic";
const BUCKET_FQN = "@winglang/sdk.cloud.Bucket";
const API_FQN = "@winglang/sdk.cloud.Api";

const createFunction = (target, scope, id, inflight, props) => {
if (inflight._inflightType === "_inflightPython") {
Expand Down Expand Up @@ -36,6 +38,10 @@ module.exports.Platform = class Platform {
if (target === "tf-aws") {
return new TfAwsBucket(scope, id, ...props);
}
} else if (type === API_FQN) {
if (target === "tf-aws") {
return new TfAwsApi(scope, id, ...props);
}
}
}
};

0 comments on commit b680f38

Please sign in to comment.