Feature/add button to test scraper configuration (#2808)

* Add button to test scraper configuration

* Update changelog

---------

Co-authored-by: Manushreshta B L <manushreshta27@gmail.com>
Co-authored-by: Hugo Persson <hugo.e.persson@gmail.com>
pull/2807/head
Thomas Kaul 5 months ago committed by GitHub
parent 3717e38845
commit 43b4f14ace
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
### Added
- Added a button to test the scraper configuration in the asset profile details dialog of the admin control
## 2.33.0 - 2023-12-31
### Added

@ -3,6 +3,7 @@ import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
import { ApiService } from '@ghostfolio/api/services/api/api.service';
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering/data-gathering.service';
import { ManualService } from '@ghostfolio/api/services/data-provider/manual/manual.service';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { PropertyDto } from '@ghostfolio/api/services/property/property.dto';
import {
@ -31,6 +32,7 @@ import {
Get,
HttpException,
Inject,
Logger,
Param,
Patch,
Post,
@ -56,6 +58,7 @@ export class AdminController {
private readonly adminService: AdminService,
private readonly apiService: ApiService,
private readonly dataGatheringService: DataGatheringService,
private readonly manualService: ManualService,
private readonly marketDataService: MarketDataService,
@Inject(REQUEST) private readonly request: RequestWithUser
) {}
@ -179,8 +182,8 @@ export class AdminController {
}
@Get('market-data')
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@HasPermission(permissions.accessAdminControl)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async getMarketData(
@Query('assetSubClasses') filterByAssetSubClasses?: string,
@Query('presetId') presetId?: MarketDataPreset,
@ -215,6 +218,30 @@ export class AdminController {
return this.adminService.getMarketDataBySymbol({ dataSource, symbol });
}
@HasPermission(permissions.accessAdminControl)
@Post('market-data/:dataSource/:symbol/test')
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async testMarketData(
@Body() data: { scraperConfiguration: string },
@Param('dataSource') dataSource: DataSource,
@Param('symbol') symbol: string
): Promise<{ price: number }> {
try {
const { headers, selector, url } = JSON.parse(data.scraperConfiguration);
const price = await this.manualService.test({ headers, selector, url });
if (price) {
return { price };
}
throw new Error('Could not parse the current market price');
} catch (error) {
Logger.error(error);
throw new HttpException(error.message, StatusCodes.BAD_REQUEST);
}
}
@HasPermission(permissions.accessAdminControl)
@Post('market-data/:dataSource/:symbol')
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)

@ -74,6 +74,6 @@ import { DataProviderService } from './data-provider.service';
},
YahooFinanceDataEnhancerService
],
exports: [DataProviderService, YahooFinanceService]
exports: [DataProviderService, ManualService, YahooFinanceService]
})
export class DataProviderModule {}

@ -18,7 +18,7 @@ import { DataSource, SymbolProfile } from '@prisma/client';
import * as cheerio from 'cheerio';
import { isUUID } from 'class-validator';
import { addDays, format, isBefore } from 'date-fns';
import got from 'got';
import got, { Headers } from 'got';
@Injectable()
export class ManualService implements DataProviderInterface {
@ -97,21 +97,7 @@ export class ManualService implements DataProviderInterface {
return {};
}
const abortController = new AbortController();
setTimeout(() => {
abortController.abort();
}, this.configurationService.get('REQUEST_TIMEOUT'));
const { body } = await got(url, {
headers,
// @ts-ignore
signal: abortController.signal
});
const $ = cheerio.load(body);
const value = extractNumberFromString($(selector).text());
const value = await this.scrape({ headers, selector, url });
return {
[symbol]: {
@ -233,4 +219,42 @@ export class ManualService implements DataProviderInterface {
return { items };
}
public async test(params: any) {
return this.scrape({
headers: params.headers,
selector: params.selector,
url: params.url
});
}
private async scrape({
headers = {},
selector,
url
}: {
headers?: Headers;
selector: string;
url: string;
}): Promise<number> {
try {
const abortController = new AbortController();
setTimeout(() => {
abortController.abort();
}, this.configurationService.get('REQUEST_TIMEOUT'));
const { body } = await got(url, {
headers,
// @ts-ignore
signal: abortController.signal
});
const $ = cheerio.load(body);
return extractNumberFromString($(selector).first().text());
} catch (error) {
throw error;
}
}
}

@ -277,6 +277,34 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
});
}
public onTestMarketData() {
this.adminService
.testMarketData({
dataSource: this.data.dataSource,
scraperConfiguration:
this.assetProfileForm.controls['scraperConfiguration'].value,
symbol: this.data.symbol
})
.pipe(
catchError(({ error }) => {
alert(`Error: ${error?.message}`);
return EMPTY;
}),
takeUntil(this.unsubscribeSubject)
)
.subscribe(({ price }) => {
alert(
$localize`The current market price is` +
' ' +
price +
' ' +
(<Currency>(
(<unknown>this.assetProfileForm.controls['currency'].value)
))?.value
);
});
}
public onUnsetBenchmark({ dataSource, symbol }: UniqueAsset) {
this.dataService
.deleteBenchmark({ dataSource, symbol })

@ -243,12 +243,24 @@
<div *ngIf="assetProfile?.dataSource === 'MANUAL'">
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Scraper Configuration</mat-label>
<textarea
cdkTextareaAutosize
formControlName="scraperConfiguration"
matInput
type="text"
></textarea>
<div class="align-items-end d-flex">
<textarea
cdkTextareaAutosize
formControlName="scraperConfiguration"
matInput
type="text"
(keyup.enter)="$event.stopPropagation()"
></textarea>
<button
color="accent"
mat-flat-button
type="button"
[disabled]="assetProfileForm.controls['scraperConfiguration'].value === '{}'"
(click)="onTestMarketData()"
>
<ng-container i18n>Test</ng-container>
</button>
</div>
</mat-form-field>
</div>
<div>

@ -259,4 +259,17 @@ export class AdminService {
public putTag(aTag: UpdateTagDto) {
return this.http.put<Tag>(`/api/v1/tag/${aTag.id}`, aTag);
}
public testMarketData({
dataSource,
scraperConfiguration,
symbol
}: UniqueAsset & UpdateAssetProfileDto['scraperConfiguration']) {
return this.http.post<any>(
`/api/v1/admin/market-data/${dataSource}/${symbol}/test`,
{
scraperConfiguration
}
);
}
}

Loading…
Cancel
Save