Skip to content

Commit 766c647

Browse files
mhamasvladfrangu
andauthored
feat: actor charge command (#748)
Co-authored-by: Vlad Frangu <[email protected]>
1 parent 076fa50 commit 766c647

File tree

3 files changed

+115
-13
lines changed

3 files changed

+115
-13
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,5 @@ test/tmp
2424
oclif.manifest.json
2525
tmp
2626

27+
package-lock.json
2728
scripts/temporary-reference.md

src/commands/actor/charge.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { APIFY_ENV_VARS } from '@apify/consts';
2+
import { Args, Flags } from '@oclif/core';
3+
4+
import { getApifyTokenFromEnvOrAuthFile } from '../../lib/actor.js';
5+
import { ApifyCommand } from '../../lib/apify_command.js';
6+
import { info } from '../../lib/outputs.js';
7+
import { getLoggedClient } from '../../lib/utils.js';
8+
9+
/**
10+
* This command can be used to charge for a specific event in the pay-per-event Actor run.
11+
* - If run locally or with the --test-pay-per-event flag, it will only log the charge request, without actually charging.
12+
* - If run in the Actor run on Apify platform without the testing flag, it will charge the specified amount of events.
13+
*
14+
* Future TODOs:
15+
* - Add logic to work with the max charge USD to prevent exceeding the charging limit.
16+
* - Add logic to store events in the log dataset for later inspection to aid local development.
17+
*/
18+
export class ChargeCommand extends ApifyCommand<typeof ChargeCommand> {
19+
static override description = 'Charge for a specific event in the pay-per-event Actor run.';
20+
21+
static override args = {
22+
eventName: Args.string({
23+
description: 'Name of the event to charge for',
24+
required: true,
25+
}),
26+
};
27+
28+
static override flags = {
29+
'count': Flags.integer({
30+
description: 'Number of events to charge',
31+
required: false,
32+
default: 1,
33+
}),
34+
'idempotency-key': Flags.string({
35+
description: 'Idempotency key for the charge request',
36+
required: false,
37+
}),
38+
'test-pay-per-event': Flags.boolean({
39+
description: 'Test pay-per-event charging without actually charging',
40+
required: false,
41+
default: false,
42+
}),
43+
};
44+
45+
async run() {
46+
const { eventName } = this.args;
47+
const { count, testPayPerEvent, idempotencyKey } = this.flags;
48+
49+
const isAtHome = Boolean(process.env.APIFY_IS_AT_HOME);
50+
51+
if (!isAtHome) {
52+
info({
53+
message: `No platform detected: would charge ${count} events of type "${eventName}" with idempotency key "${idempotencyKey ?? 'not-provided'}".`,
54+
stdout: true,
55+
});
56+
return;
57+
}
58+
59+
if (testPayPerEvent) {
60+
info({
61+
message: `PPE test mode: would charge ${count} events of type "${eventName}" with idempotency key "${idempotencyKey ?? 'not-provided'}".`,
62+
stdout: true,
63+
});
64+
return;
65+
}
66+
67+
const apifyToken = await getApifyTokenFromEnvOrAuthFile();
68+
const apifyClient = await getLoggedClient(apifyToken);
69+
if (!apifyClient) {
70+
throw new Error('Apify token is not set. Please set it using the environment variable APIFY_TOKEN.');
71+
}
72+
const runId = process.env[APIFY_ENV_VARS.ACTOR_RUN_ID];
73+
74+
if (!runId) {
75+
throw new Error('Charge command must be executed in a running Actor. Run ID not found.');
76+
}
77+
78+
const run = await apifyClient.run(runId).get();
79+
if (run?.pricingInfo?.pricingModel !== 'PAY_PER_EVENT') {
80+
throw new Error('Charge command can only be used with pay-per-event pricing model.');
81+
}
82+
83+
info({
84+
message: `Charging ${count} events of type "${eventName}" with idempotency key "${idempotencyKey ?? 'not-provided'}" (runId: ${runId}).`,
85+
stdout: true,
86+
});
87+
await apifyClient.run(runId).charge({
88+
eventName,
89+
count,
90+
idempotencyKey,
91+
});
92+
}
93+
}

src/lib/actor.ts

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,26 @@ export const APIFY_STORAGE_TYPES = {
1616
REQUEST_QUEUE: 'REQUEST_QUEUE',
1717
} as const;
1818

19+
/**
20+
* Returns Apify token from environment variable or local auth file.
21+
* @returns Apify token
22+
*/
23+
export const getApifyTokenFromEnvOrAuthFile = async () => {
24+
const apifyToken = process.env[APIFY_ENV_VARS.TOKEN];
25+
if (apifyToken) {
26+
return apifyToken;
27+
}
28+
29+
const localUserInfo = await getLocalUserInfo();
30+
if (!localUserInfo || !localUserInfo.token) {
31+
throw new Error(
32+
'Apify token is not set. Please set it using the environment variable APIFY_TOKEN or apify login command.',
33+
);
34+
}
35+
36+
return localUserInfo.token;
37+
};
38+
1939
/**
2040
* Returns instance of ApifyClient or ApifyStorageLocal based on environment variables.
2141
* @param options - ApifyClient options
@@ -33,19 +53,7 @@ export const getApifyStorageClient = async (
3353
...options,
3454
});
3555
}
36-
37-
// NOTE: Token in env var overrides token in local auth file.
38-
let apifyToken = process.env[APIFY_ENV_VARS.TOKEN];
39-
if (!apifyToken) {
40-
const localUserInfo = await getLocalUserInfo();
41-
if (!localUserInfo || !localUserInfo.token) {
42-
throw new Error(
43-
'Apify token is not set. Please set it using the environment variable APIFY_TOKEN or apify login command.',
44-
);
45-
}
46-
47-
apifyToken = localUserInfo.token;
48-
}
56+
const apifyToken = await getApifyTokenFromEnvOrAuthFile();
4957

5058
return new ApifyClient({
5159
...getApifyClientOptions(apifyToken),

0 commit comments

Comments
 (0)