Skip to content

Commit 0f4b3f0

Browse files
authored
feat: actors info (#701)
1 parent 5594ce6 commit 0f4b3f0

File tree

1 file changed

+298
-0
lines changed

1 file changed

+298
-0
lines changed

src/commands/actors/info.ts

Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
import { Args, Flags } from '@oclif/core';
2+
import type { Actor, ActorTaggedBuild, Build, User } from 'apify-client';
3+
import chalk from 'chalk';
4+
5+
import { ApifyCommand } from '../../lib/apify_command.js';
6+
import { resolveActorContext } from '../../lib/commands/resolve-actor-context.js';
7+
import { CompactMode, ResponsiveTable } from '../../lib/commands/responsive-table.js';
8+
import { error, simpleLog } from '../../lib/outputs.js';
9+
import { DurationFormatter, getLoggedClientOrThrow, TimestampFormatter } from '../../lib/utils.js';
10+
11+
interface HydratedActorInfo extends Omit<Actor, 'taggedBuilds'> {
12+
taggedBuilds?: Record<string, ActorTaggedBuild & { build?: Build }>;
13+
actorMaker?: User;
14+
}
15+
16+
interface PricingInfo {
17+
pricingModel: 'PRICE_PER_DATASET_ITEM' | 'FLAT_PRICE_PER_MONTH' | 'PAY_PER_EVENT' | 'FREE';
18+
pricePerUnitUsd: number;
19+
unitName: string;
20+
startedAt: string;
21+
createdAt: string;
22+
apifyMarginPercentage: number;
23+
notifiedAboutFutureChangeAt: string;
24+
notifiedAboutChangeAt: string;
25+
trialMinutes?: number;
26+
pricingPerEvent?: {
27+
actorChargeEvents: Record<string, { eventTitle: string; eventDescription: string; eventPriceUsd: number }>;
28+
};
29+
}
30+
31+
const eventTitleColumn = '\u200b';
32+
const eventPriceUsdColumn = '\u200b\u200b';
33+
34+
const payPerEventTable = new ResponsiveTable({
35+
allColumns: [eventTitleColumn, eventPriceUsdColumn],
36+
mandatoryColumns: [eventTitleColumn, eventPriceUsdColumn],
37+
columnAlignments: {
38+
[eventTitleColumn]: 'left',
39+
[eventPriceUsdColumn]: 'right',
40+
},
41+
});
42+
43+
export class ActorsInfoCommand extends ApifyCommand<typeof ActorsInfoCommand> {
44+
static override description = 'Get information about an Actor.';
45+
46+
static override flags = {
47+
readme: Flags.boolean({
48+
description: 'Return the Actor README.',
49+
exclusive: ['input'],
50+
}),
51+
input: Flags.boolean({
52+
description: 'Return the Actor input schema.',
53+
exclusive: ['readme'],
54+
}),
55+
};
56+
57+
static override args = {
58+
actorId: Args.string({
59+
description: 'The ID of the Actor to return information about.',
60+
required: true,
61+
}),
62+
};
63+
64+
static override enableJsonFlag = true;
65+
66+
async run() {
67+
const { actorId } = this.args;
68+
const { readme, input, json } = this.flags;
69+
70+
const client = await getLoggedClientOrThrow();
71+
const ctx = await resolveActorContext({ providedActorNameOrId: actorId, client });
72+
73+
if (!ctx.valid) {
74+
error({
75+
message: `${ctx.reason}. Please specify the Actor ID.`,
76+
stdout: true,
77+
});
78+
79+
return;
80+
}
81+
82+
const actorInfo = (await client.actor(ctx.id).get())! as HydratedActorInfo;
83+
const actorMaker = await client.user(actorInfo.userId).get();
84+
85+
actorInfo.actorMaker = actorMaker;
86+
87+
// Hydrate builds
88+
for (const taggedBuild of Object.values(actorInfo.taggedBuilds ?? {})) {
89+
if (!taggedBuild.buildId) {
90+
continue;
91+
}
92+
93+
const buildData = await client.build(taggedBuild.buildId).get();
94+
95+
taggedBuild.build = buildData;
96+
}
97+
98+
if (json) {
99+
return actorInfo;
100+
}
101+
102+
const latest = actorInfo.taggedBuilds?.latest;
103+
104+
if (readme) {
105+
if (!latest) {
106+
error({
107+
message: 'No README found for this Actor.',
108+
stdout: true,
109+
});
110+
111+
return;
112+
}
113+
114+
if (!latest.build?.readme) {
115+
error({
116+
message: 'No README found for this Actor.',
117+
stdout: true,
118+
});
119+
120+
return;
121+
}
122+
123+
simpleLog({ message: latest.build.readme, stdout: true });
124+
}
125+
126+
if (input) {
127+
if (!latest) {
128+
error({
129+
message: 'No input schema found for this Actor.',
130+
stdout: true,
131+
});
132+
133+
return;
134+
}
135+
136+
if (!latest.build?.inputSchema) {
137+
error({
138+
message: 'No input schema found for this Actor.',
139+
stdout: true,
140+
});
141+
142+
return;
143+
}
144+
145+
simpleLog({ message: latest.build.inputSchema, stdout: true });
146+
}
147+
148+
const message = [
149+
`Information about Actor ${chalk.yellow(`${actorInfo.username}/${actorInfo.name}`)} (${chalk.gray(actorInfo.id)})`,
150+
'',
151+
];
152+
153+
if (actorInfo.title) {
154+
message.push(`${chalk.yellow('Title:')} ${chalk.bold(actorInfo.title)}`);
155+
}
156+
157+
if (actorInfo.description) {
158+
message.push(`${chalk.yellow('Description:')} ${actorInfo.description}`);
159+
}
160+
161+
message.push(
162+
`${chalk.yellow('Created at:')} ${chalk.cyan(TimestampFormatter.display(actorInfo.createdAt))} ${chalk.gray('|')} ${chalk.yellow('Updated at:')} ${chalk.cyan(
163+
TimestampFormatter.display(actorInfo.modifiedAt),
164+
)}`,
165+
);
166+
167+
if (actorInfo.actorMaker) {
168+
message.push(
169+
'',
170+
`${chalk.yellow('Made by:')} ${chalk.cyan(actorInfo.actorMaker.profile.name ?? actorInfo.actorMaker.username)}`,
171+
);
172+
173+
// Missing types who?
174+
if (Reflect.get(actorInfo, 'isCritical')) {
175+
message[message.length - 1] += ` ${chalk.bgGray('Maintained by Apify')}`;
176+
}
177+
}
178+
179+
if (actorInfo.isPublic) {
180+
message.push('', `${chalk.yellow('Actor is')} ${chalk.green('PUBLIC')}`);
181+
} else {
182+
message.push('', `${chalk.yellow('Actor is')} ${chalk.cyan('PRIVATE')}`);
183+
}
184+
185+
if (actorInfo.isDeprecated) {
186+
message.push('', `${chalk.yellow('Actor is')} ${chalk.red('DEPRECATED')}`);
187+
}
188+
189+
// Pricing info
190+
const pricingInfo = Reflect.get(actorInfo, 'pricingInfos') as PricingInfo[] | undefined;
191+
192+
if (pricingInfo?.length) {
193+
// We only print the latest pricing info
194+
const latestPricingInfo = pricingInfo.at(-1)!;
195+
196+
switch (latestPricingInfo.pricingModel) {
197+
case 'FLAT_PRICE_PER_MONTH': {
198+
message.push(
199+
`${chalk.yellow('Pricing information:')} ${chalk.bgGray(`$${latestPricingInfo.pricePerUnitUsd}/month + usage`)}`,
200+
);
201+
202+
if (latestPricingInfo.trialMinutes) {
203+
const minutesToMs = latestPricingInfo.trialMinutes * 60 * 1000;
204+
const duration = DurationFormatter.format(minutesToMs);
205+
206+
message.push(` ${chalk.yellow('Trial duration:')} ${chalk.bold(duration)}`);
207+
}
208+
209+
break;
210+
}
211+
case 'PRICE_PER_DATASET_ITEM': {
212+
const pricePerOneKItems = latestPricingInfo.pricePerUnitUsd * 1000;
213+
214+
message.push(
215+
`${chalk.yellow('Pricing information:')} ${chalk.bgGray(`$${pricePerOneKItems.toFixed(2)} / 1,000 results`)}`,
216+
);
217+
218+
break;
219+
}
220+
case 'PAY_PER_EVENT': {
221+
message.push(`${chalk.yellow('Pricing information:')} ${chalk.bgGray('Pay per event')}`);
222+
223+
const events = Object.values(latestPricingInfo.pricingPerEvent?.actorChargeEvents ?? {});
224+
225+
for (const eventInfo of events) {
226+
payPerEventTable.pushRow({
227+
[eventTitleColumn]: eventInfo.eventTitle,
228+
[eventPriceUsdColumn]: chalk.bold(`$${eventInfo.eventPriceUsd.toFixed(2)}`),
229+
});
230+
}
231+
232+
const rendered = payPerEventTable.render(CompactMode.VeryCompact);
233+
const split = rendered.split('\n');
234+
235+
// Remove the second line
236+
split.splice(1, 1);
237+
238+
message.push(split.join('\n'));
239+
240+
break;
241+
}
242+
243+
case 'FREE': {
244+
message.push(`${chalk.yellow('Pricing information:')} ${chalk.bgGray('Pay for usage')}`);
245+
break;
246+
}
247+
248+
default: {
249+
message.push(
250+
`${chalk.yellow('Pricing information:')} ${chalk.bgGray(`Unknown pricing model (${chalk.yellow(latestPricingInfo.pricingModel)})`)}`,
251+
);
252+
}
253+
}
254+
} else {
255+
message.push(`${chalk.yellow('Pricing information:')} ${chalk.bgGray('Pay for usage')}`);
256+
}
257+
258+
// TODO: do we care about this information?
259+
if (actorInfo.seoTitle || actorInfo.seoDescription) {
260+
message.push('', chalk.yellow('SEO information:'));
261+
262+
if (actorInfo.seoTitle) {
263+
message.push(` ${chalk.yellow('Title:')} ${actorInfo.seoTitle}`);
264+
}
265+
266+
if (actorInfo.seoDescription) {
267+
message.push(` ${chalk.yellow('Description:')} ${actorInfo.seoDescription}`);
268+
}
269+
}
270+
271+
if (actorInfo.taggedBuilds) {
272+
message.push('', chalk.yellow('Builds:'));
273+
274+
// Handle latest first
275+
const latestBuild = actorInfo.taggedBuilds.latest;
276+
277+
if (latestBuild) {
278+
message.push(
279+
` ${chalk.yellow('-')} ${chalk.cyan(latestBuild.buildNumber)} ${chalk.gray('/')} ${chalk.yellow('latest')}`,
280+
);
281+
}
282+
283+
for (const [buildTag, build] of Object.entries(actorInfo.taggedBuilds)) {
284+
if (buildTag === 'latest') {
285+
continue;
286+
}
287+
288+
message.push(
289+
` ${chalk.yellow('-')} ${chalk.cyan(build.buildNumber)} ${chalk.gray('/')} ${chalk.yellow(buildTag)}`,
290+
);
291+
}
292+
}
293+
294+
simpleLog({ message: message.join('\n'), stdout: true });
295+
296+
return undefined;
297+
}
298+
}

0 commit comments

Comments
 (0)