-
Notifications
You must be signed in to change notification settings - Fork 325
[OPDATA-3889] FTSE SFTP EA Downloading and Parsing #4027
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
chray-zhang
wants to merge
50
commits into
main
Choose a base branch
from
full-ea-sftp
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
50 commits
Select commit
Hold shift + click to select a range
e807511
Initial commit for split
chray-zhang 638bf65
dependencies
chray-zhang 620f1ff
Changeset
chray-zhang 73628d2
comma
chray-zhang 7d6bd04
Added transport and endpoint
chray-zhang 6129575
cleanup
chray-zhang c4452fd
cleanup
chray-zhang b631b7f
Revision
chray-zhang a53b80f
Removed unused imports
chray-zhang a8cef07
Revision for comments
chray-zhang 7fb0283
Removed unused import
chray-zhang c023388
Merge branch 'ftse-utils' into ftse-transport-endpoint-v2
chray-zhang 6cb34a0
Removed blacklist
chray-zhang db45779
Merge branch 'main' into ftse-transport-endpoint-v2
chray-zhang 8feab27
Revision
chray-zhang 51bc4ef
Removed env file
chray-zhang d8b14c7
Revision
chray-zhang 350dcc4
Removed blacklist
chray-zhang fc20d86
removed transport test
chray-zhang d4d129c
removed unused transport snap
chray-zhang b3dafb6
Fix lint
chray-zhang ecaf34c
Removed download
chray-zhang af027e9
Added back file name and path
chray-zhang 0fe3e7a
Added back blacklist
chray-zhang 8602bbb
Fix lint
chray-zhang 6b09c27
Revision
chray-zhang cc89634
renamed utils to constants
chray-zhang 9e0a4ff
Revision
chray-zhang 81592f5
revision
chray-zhang bd2db03
Revision
chray-zhang 9df9f57
Fixed any
chray-zhang dde1f42
Fixed Types
chray-zhang 28cc03f
Added Adapter to pass deploy test
chray-zhang 9911e27
Empty index.ts to pass tests
chray-zhang b9af068
index.ts
chray-zhang 4e946b0
Revert "index.ts"
chray-zhang b3419ea
Revert "Empty index.ts to pass tests"
chray-zhang e3d6eaf
Revert "Added Adapter to pass deploy test"
chray-zhang 0b87eac
Merge branch 'main' into ftse-transport-endpoint-v2
chray-zhang fea6694
Initial commit for download
chray-zhang ecd2712
[NOISSUE] Adding index.ts to ftse sftp EA to pass deploy workflow (#4…
chray-zhang a0dc7cb
Added download logic and tests
chray-zhang 873e061
Changesets
chray-zhang bf673fa
Types
chray-zhang 411d5ed
Types
chray-zhang 0552985
removed all any
chray-zhang 2915326
cleaner validation
chray-zhang 189e150
Merge branch 'ftse-transport-endpoint-v2' into full-ea-sftp
chray-zhang d9597d3
Merge branch 'main' into full-ea-sftp
chray-zhang 9a8ca73
Fixed Lint
chray-zhang File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'@chainlink/ftse-sftp-adapter': major | ||
--- | ||
|
||
Adding Downloading and parsing logic for russell and ftse csv files from ftse sftp server |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,12 @@ | ||
export * from './parsing' | ||
import { expose, ServerInstance } from '@chainlink/external-adapter-framework' | ||
import { Adapter } from '@chainlink/external-adapter-framework/adapter' | ||
import { config } from './config' | ||
import * as endpoints from './endpoint' | ||
|
||
export const adapter = new Adapter({ | ||
name: 'FTSE_SFTP', | ||
config, | ||
endpoints: [endpoints.sftp.endpoint], | ||
}) | ||
|
||
export const server = (): Promise<ServerInstance | undefined> => expose(adapter) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
export const instrumentToFilePathMap: Record<string, string> = { | ||
FTSE100INDEX: '/data/valuation/uk_all_share/', | ||
Russell1000INDEX: '/data/Returns_and_Values/Russell_US_Indexes_Daily_Index_Values_Real_Time_TXT/', | ||
Russell2000INDEX: '/data/Returns_and_Values/Russell_US_Indexes_Daily_Index_Values_Real_Time_TXT/', | ||
Russell3000INDEX: '/data/Returns_and_Values/Russell_US_Indexes_Daily_Index_Values_Real_Time_TXT/', | ||
} | ||
|
||
export const instrumentToFileTemplateMap: Record<string, string> = { | ||
FTSE100INDEX: 'ukallv*.csv', | ||
Russell1000INDEX: 'daily_values_russell_*.CSV', | ||
Russell2000INDEX: 'daily_values_russell_*.CSV', | ||
Russell3000INDEX: 'daily_values_russell_*.CSV', | ||
} | ||
|
||
export const instrumentToFileRegexMap: Record<string, RegExp> = { | ||
FTSE100INDEX: /^ukallv\d{4}\.csv$/, | ||
Russell1000INDEX: /^daily_values_russell_\d{6}\.CSV$/, | ||
Russell2000INDEX: /^daily_values_russell_\d{6}\.CSV$/, | ||
Russell3000INDEX: /^daily_values_russell_\d{6}\.CSV$/, | ||
} | ||
|
||
/** | ||
* Validates if an instrument is supported by checking if it has all required mappings | ||
* @param instrument The instrument identifier to validate | ||
* @returns true if the instrument is supported, false otherwise | ||
*/ | ||
export function isInstrumentSupported(instrument: string): boolean { | ||
return !!(instrumentToFilePathMap[instrument] && instrumentToFileRegexMap[instrument]) | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,13 +1,35 @@ | ||
import { EndpointContext } from '@chainlink/external-adapter-framework/adapter' | ||
import { ResponseCache } from '@chainlink/external-adapter-framework/cache/response' | ||
import { TransportDependencies } from '@chainlink/external-adapter-framework/transports' | ||
import { SubscriptionTransport } from '@chainlink/external-adapter-framework/transports/abstract/subscription' | ||
import { sleep } from '@chainlink/external-adapter-framework/util' | ||
import SftpClient from 'ssh2-sftp-client' | ||
import { BaseEndpointTypes } from '../endpoint/sftp' | ||
import { AdapterResponse, makeLogger, sleep } from '@chainlink/external-adapter-framework/util' | ||
import { AdapterInputError } from '@chainlink/external-adapter-framework/validation/error' | ||
import SftpClient, { FileInfo } from 'ssh2-sftp-client' | ||
import { BaseEndpointTypes, IndexResponseData, inputParameters } from '../endpoint/sftp' | ||
import { CSVParserFactory } from '../parsing/factory' | ||
import { | ||
instrumentToFilePathMap, | ||
instrumentToFileRegexMap, | ||
isInstrumentSupported, | ||
} from './constants' | ||
|
||
const logger = makeLogger('FTSE SFTP Adapter') | ||
|
||
type RequestParams = typeof inputParameters.validated | ||
|
||
interface SftpConnectionConfig { | ||
host: string | ||
port: number | ||
username: string | ||
password: string | ||
readyTimeout: number | ||
} | ||
|
||
export class SftpTransport extends SubscriptionTransport<BaseEndpointTypes> { | ||
config!: BaseEndpointTypes['Settings'] | ||
endpointName!: string | ||
name!: string | ||
responseCache!: ResponseCache<BaseEndpointTypes> | ||
sftpClient: SftpClient | ||
|
||
constructor() { | ||
|
@@ -24,12 +46,156 @@ export class SftpTransport extends SubscriptionTransport<BaseEndpointTypes> { | |
await super.initialize(dependencies, adapterSettings, endpointName, transportName) | ||
this.config = adapterSettings | ||
this.endpointName = endpointName | ||
this.name = transportName | ||
this.responseCache = dependencies.responseCache | ||
} | ||
|
||
async backgroundHandler(context: EndpointContext<BaseEndpointTypes>): Promise<void> { | ||
async backgroundHandler( | ||
context: EndpointContext<BaseEndpointTypes>, | ||
entries: RequestParams[], | ||
): Promise<void> { | ||
await Promise.all(entries.map(async (param) => this.handleRequest(param))) | ||
await sleep(context.adapterSettings.BACKGROUND_EXECUTE_MS) | ||
} | ||
|
||
async handleRequest(param: RequestParams) { | ||
let response: AdapterResponse<BaseEndpointTypes['Response']> | ||
try { | ||
response = await this._handleRequest(param) | ||
} catch (e) { | ||
const errorMessage = e instanceof Error ? e.message : 'Unknown error occurred' | ||
response = { | ||
statusCode: 502, | ||
errorMessage, | ||
timestamps: { | ||
providerDataRequestedUnixMs: 0, | ||
providerDataReceivedUnixMs: 0, | ||
providerIndicatedTimeUnixMs: undefined, | ||
}, | ||
} | ||
} finally { | ||
try { | ||
await this.sftpClient.end() | ||
logger.info('SFTP connection closed') | ||
} catch (error) { | ||
logger.error('Error closing SFTP connection:', error) | ||
} | ||
} | ||
|
||
await this.responseCache.write(this.name, [{ params: param, response }]) | ||
} | ||
|
||
async _handleRequest( | ||
param: RequestParams, | ||
): Promise<AdapterResponse<BaseEndpointTypes['Response']>> { | ||
const providerDataRequestedUnixMs = Date.now() | ||
|
||
await this.connectToSftp() | ||
|
||
const parsedData = await this.tryDownloadAndParseFile(param.instrument) | ||
|
||
// Extract the numeric result based on the data type | ||
let result: number | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. According to ResponseGeneric we need some sort of |
||
if ('gbpIndex' in parsedData) { | ||
// FTSE data | ||
result = (parsedData.gbpIndex as number) ?? 0 | ||
} else if ('close' in parsedData) { | ||
// Russell data | ||
result = parsedData.close as number | ||
} else { | ||
throw new Error('Unknown data format received from parser') | ||
} | ||
|
||
logger.info(`Successfully processed data for instrument: ${param.instrument}`) | ||
return { | ||
data: { | ||
result: parsedData, | ||
}, | ||
statusCode: 200, | ||
result, | ||
timestamps: { | ||
providerDataRequestedUnixMs, | ||
providerDataReceivedUnixMs: Date.now(), | ||
providerIndicatedTimeUnixMs: undefined, | ||
}, | ||
} | ||
} | ||
|
||
private async connectToSftp(): Promise<void> { | ||
const connectConfig: SftpConnectionConfig = { | ||
host: this.config.SFTP_HOST, | ||
port: this.config.SFTP_PORT || 22, | ||
username: this.config.SFTP_USERNAME, | ||
password: this.config.SFTP_PASSWORD, | ||
readyTimeout: 30000, | ||
} | ||
|
||
try { | ||
// Create a new client instance to avoid connection state issues | ||
this.sftpClient = new SftpClient() | ||
await this.sftpClient.connect(connectConfig) | ||
logger.info('Successfully connected to SFTP server') | ||
} catch (error) { | ||
logger.error(error, 'Failed to connect to SFTP server') | ||
throw new AdapterInputError({ | ||
statusCode: 500, | ||
message: `Failed to connect to SFTP server: ${ | ||
error instanceof Error ? error.message : 'Unknown error' | ||
}`, | ||
}) | ||
} | ||
} | ||
|
||
private async tryDownloadAndParseFile(instrument: string): Promise<IndexResponseData> { | ||
// Validate that the instrument is supported | ||
if (!isInstrumentSupported(instrument)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe I should move this validation up to |
||
throw new AdapterInputError({ | ||
statusCode: 400, | ||
message: `Unsupported instrument: ${instrument}`, | ||
}) | ||
} | ||
|
||
const filePath = instrumentToFilePathMap[instrument] | ||
const fileRegex = instrumentToFileRegexMap[instrument] | ||
|
||
const fileList = await this.sftpClient.list(filePath) | ||
// Filter files based on the regex pattern | ||
const matchingFiles = fileList | ||
.map((file: FileInfo) => file.name) | ||
.filter((fileName: string) => fileRegex.test(fileName)) | ||
|
||
if (matchingFiles.length === 0) { | ||
throw new AdapterInputError({ | ||
statusCode: 500, | ||
message: `No files matching pattern ${fileRegex} found in directory: ${filePath}`, | ||
}) | ||
} else if (matchingFiles.length > 1) { | ||
throw new AdapterInputError({ | ||
statusCode: 500, | ||
message: `Multiple files matching pattern ${fileRegex} found in directory: ${filePath}.`, | ||
}) | ||
} | ||
const fullPath = `${filePath}${matchingFiles[0]}` | ||
|
||
// Log the download attempt | ||
logger.info(`Downloading file: ${fullPath}`) | ||
|
||
const fileContent = await this.sftpClient.get(fullPath) | ||
// we need latin1 here because the file contains special characters like "®" | ||
const csvContent = fileContent.toString('latin1') | ||
|
||
const parser = CSVParserFactory.detectParserByInstrument(instrument) | ||
|
||
if (!parser) { | ||
throw new AdapterInputError({ | ||
statusCode: 500, | ||
message: `Parser initialization failed for instrument: ${instrument}`, | ||
}) | ||
} | ||
|
||
return (await parser.parse(csvContent)) as IndexResponseData | ||
} | ||
|
||
getSubscriptionTtlFromConfig(adapterSettings: BaseEndpointTypes['Settings']): number { | ||
return adapterSettings.BACKGROUND_EXECUTE_MS || 60000 | ||
} | ||
|
72 changes: 72 additions & 0 deletions
72
packages/sources/ftse-sftp/test/integration/__snapshots__/adapter.test.ts.snap
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
// Jest Snapshot v1, https://goo.gl/fbAQLP | ||
|
||
exports[`execute ftse_sftp endpoint should return success for FTSE100INDEX 1`] = ` | ||
{ | ||
"data": { | ||
"result": { | ||
"gbpIndex": 8045.12345678, | ||
"indexBaseCurrency": "GBP", | ||
"indexCode": "UKX", | ||
"indexSectorName": "FTSE 100 Index", | ||
"numberOfConstituents": 100, | ||
}, | ||
}, | ||
"result": 8045.12345678, | ||
"statusCode": 200, | ||
"timestamps": { | ||
"providerDataReceivedUnixMs": 1641035471111, | ||
"providerDataRequestedUnixMs": 1641035471111, | ||
}, | ||
} | ||
`; | ||
|
||
exports[`execute ftse_sftp endpoint should return success for Russell1000INDEX 1`] = ` | ||
{ | ||
"data": { | ||
"result": { | ||
"close": 2654.123456, | ||
"indexName": "Russell 1000® Index", | ||
}, | ||
}, | ||
"result": 2654.123456, | ||
"statusCode": 200, | ||
"timestamps": { | ||
"providerDataReceivedUnixMs": 1641035471111, | ||
"providerDataRequestedUnixMs": 1641035471111, | ||
}, | ||
} | ||
`; | ||
|
||
exports[`execute ftse_sftp endpoint should return success for Russell2000INDEX 1`] = ` | ||
{ | ||
"data": { | ||
"result": { | ||
"close": 1987.654321, | ||
"indexName": "Russell 2000® Index", | ||
}, | ||
}, | ||
"result": 1987.654321, | ||
"statusCode": 200, | ||
"timestamps": { | ||
"providerDataReceivedUnixMs": 1641035471111, | ||
"providerDataRequestedUnixMs": 1641035471111, | ||
}, | ||
} | ||
`; | ||
|
||
exports[`execute ftse_sftp endpoint should return success for Russell3000INDEX 1`] = ` | ||
{ | ||
"data": { | ||
"result": { | ||
"close": 3456.789012, | ||
"indexName": "Russell 3000® Index", | ||
}, | ||
}, | ||
"result": 3456.789012, | ||
"statusCode": 200, | ||
"timestamps": { | ||
"providerDataReceivedUnixMs": 1641035471111, | ||
"providerDataRequestedUnixMs": 1641035471111, | ||
}, | ||
} | ||
`; |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to close the client when we're done or else we'll run into

end Event raised
for a hanging sftp connection