diff --git a/include/tadah/mlip/key_storage/environment_key_storage.h b/include/tadah/mlip/key_storage/environment_key_storage.h new file mode 100644 index 0000000000000000000000000000000000000000..258e13beb4358a2cf92d765d47cb2bc99e615dee --- /dev/null +++ b/include/tadah/mlip/key_storage/environment_key_storage.h @@ -0,0 +1,43 @@ +/** + * @file EnvironmentKeyStorage.h + * @brief Implementation of IKeyStorage that reads/writes keys from/to + * environment variables. + * + * By default, environment variables are read-only for user’s global + * environment. Storing can be done via setenv for the current process if + * desired. + */ + +#ifndef ENVIRONMENTKEYSTORAGE_H +#define ENVIRONMENTKEYSTORAGE_H + +#include <tadah/mlip/key_storage/ikey_storage.h> +#include <string> + +/** + * @class EnvironmentKeyStorage + * @brief Reads a key from environment variables. + * + * Storing is optional: if storing is supported, it uses setenv to update + * the environment variable only for the current process. + */ +class EnvironmentKeyStorage : public IKeyStorage { +public: + /** + * @brief Retrieves the environment variable named @p keyName. + * @param keyName Name of the env variable. + * @return The environment variable's value, or empty if not set. + */ + std::string retrieveKey(const std::string &keyName) override; + + /** + * @brief Stores a value in the process's environment using setenv. + * @param keyName Name of the env variable. + * @param keyValue Value to set. + * @return true if setenv succeeds, false otherwise. + */ + bool storeKey(const std::string &keyName, + const std::string &keyValue) override; +}; + +#endif // ENVIRONMENTKEYSTORAGE_H diff --git a/include/tadah/mlip/key_storage/fallback_key_storage.h b/include/tadah/mlip/key_storage/fallback_key_storage.h new file mode 100644 index 0000000000000000000000000000000000000000..45e94fcc239a724590130fb81597ea5e91ac9167 --- /dev/null +++ b/include/tadah/mlip/key_storage/fallback_key_storage.h @@ -0,0 +1,51 @@ +/** + * @file FallbackKeyStorage.h + * @brief An aggregator that tries multiple IKeyStorage instances in order (for reading), + * and writes to a chosen target storage. + */ + +#ifndef FALLBACKKEYSTORAGE_H +#define FALLBACKKEYSTORAGE_H + +#include <tadah/mlip/key_storage/ikey_storage.h> +#include <vector> +#include <memory> + +/** + * @class FallbackKeyStorage + * @brief Aggregates multiple IKeyStorage implementations in a fallback chain. + * + * Reading: The first storage that returns a non-empty string is used. + * Writing: By default, writes to the last storage in the list. + * (This can be adjusted if a different policy is needed.) + */ +class FallbackKeyStorage : public IKeyStorage +{ +public: + /** + * @brief Constructs with an ordered list of storages to try for retrieval. + * @param storages A vector of storages, from highest to lowest precedence for reading. + */ + FallbackKeyStorage(std::vector<std::unique_ptr<IKeyStorage>> storages); + + /** + * @brief Retrieves the key by checking storages in order, returning the first non-empty result. + * @param keyName Name of the key to retrieve. + * @return Key value, or empty string if none of the storages have it. + */ + std::string retrieveKey(const std::string& keyName) override; + + /** + * @brief Stores the key in the last storage by default. + * @param keyName Name of the key. + * @param keyValue Value to store. + * @return true if stored successfully, false otherwise. + */ + bool storeKey(const std::string& keyName, const std::string& keyValue) override; + +private: + std::vector<std::unique_ptr<IKeyStorage>> m_storages; +}; + +#endif // FALLBACKKEYSTORAGE_H + diff --git a/include/tadah/mlip/key_storage/ikey_storage.h b/include/tadah/mlip/key_storage/ikey_storage.h new file mode 100644 index 0000000000000000000000000000000000000000..0ce8276c2a4400573883a604a77f86ed884846db --- /dev/null +++ b/include/tadah/mlip/key_storage/ikey_storage.h @@ -0,0 +1,38 @@ +/** + * @file IKeyStorage.h + * @brief Abstract interface for retrieving and storing keys (e.g., API tokens). + */ + +#ifndef IKEYSTORAGE_H +#define IKEYSTORAGE_H + +#include <string> + +/** + * @class IKeyStorage + * @brief Contract for any key storage mechanism. + * + * Classes implementing this interface can store and retrieve keys by name. + */ +class IKeyStorage +{ +public: + virtual ~IKeyStorage() = default; + + /** + * @brief Retrieves the value associated with @p keyName. + * @param keyName The name of the key to be retrieved. + * @return The value if found, otherwise an empty string. + */ + virtual std::string retrieveKey(const std::string& keyName) = 0; + + /** + * @brief Stores or updates the value for @p keyName. + * @param keyName The key name (e.g. "MY_REST_API_KEY"). + * @param keyValue The value to store (e.g. "abc123"). + * @return true if successfully stored, false if not supported or an error occurred. + */ + virtual bool storeKey(const std::string& keyName, const std::string& keyValue) = 0; +}; + +#endif // IKEYSTORAGE_H diff --git a/include/tadah/mlip/key_storage/key_manager.h b/include/tadah/mlip/key_storage/key_manager.h new file mode 100644 index 0000000000000000000000000000000000000000..be98eccf6493d382bd94fcc4b0c4fce92d37cee6 --- /dev/null +++ b/include/tadah/mlip/key_storage/key_manager.h @@ -0,0 +1,85 @@ +/** + * @file KeyManager.h + * @brief Provides methods to retrieve and manage API keys for services like (MP, COD, AFLOW), + * offering a 4-option prompt when no key is found. + */ + +#ifndef KEYMANAGER_H +#define KEYMANAGER_H + +#include <string> + +/** + * @enum ServiceType + * @brief Enumerates supported services for which the KeyManager can retrieve API keys. + */ +enum class ServiceType { + MP, ///< Materials Project + COD, ///< Crystallography Open Database + AFLOW, ///< AFLOW Project + // Add more if needed +}; + +/** + * @class KeyManager + * @brief Central utility for retrieving (and if missing, prompting for) service API keys. + * + * Uses "EnvThenFile" fallback: + * 1) Environment variable (e.g. MP_REST_API_KEY) + * 2) Plain-text config in ~/.config/tadah/keys + * + * If still missing, shows four options: + * [1] Store key in config file (persistent) + * [2] Temporarily set environment variable for this run only + * [3] Show how to export it in the shell (so it can be used across multiple runs) + * [4] Abort (no key) + */ +class KeyManager { +public: + /** + * @brief Retrieves the API key for the specified service. + * + * If the key is not found in environment or config, and @p allowPrompt is true, + * an interactive menu of four options is displayed: + * 1) Store key in config file (persist for future runs), + * 2) Temporarily set the environment variable for *this* run only, + * 3) Show instructions on how to export for the shell (useful across runs), + * 4) Abort (do nothing, return empty). + * + * @param service The target service (MP, COD, AFLOW). + * @param allowPrompt If false, no prompts occur; the method returns empty if key is missing. + * @return The acquired key, or an empty string if user aborts or no key provided. + */ + static std::string getServiceKey(ServiceType service, bool allowPrompt = true); + +private: + /// Maps the enum to the environment variable name (e.g. "MP_REST_API_KEY"). + static std::string resolveKeyName(ServiceType service); + + /// A friendly string for prompts/logs (e.g. "Materials Project"). + static std::string getServiceFriendlyName(ServiceType service); + + /// Shows the four-option menu and returns the user's choice (1..4). + static int promptUserChoice(); + + /** + * @brief Sets the environment variable in the current process only, + * so it is visible to subsequent code in this process but not in the user's shell. + * + * @param keyName The environment variable name (e.g. "MP_REST_API_KEY"). + * @param keyValue The value to set. + */ + static void setEnvCurrentProcess(const std::string& keyName, const std::string& keyValue); + + /** + * @brief Shows instructions for exporting in the user’s shell so the + * variable persists across multiple runs *in that same terminal session.* + * + * Also hints how to place it in shell configuration (e.g. ~/.bashrc) if desired. + * + * @param keyName The env variable name. + * @param keyValue The key value to be exported. + */ + static void showShellExportInstructions(const std::string& keyName, const std::string& keyValue); +}; +#endif // KEYMANAGER_H diff --git a/include/tadah/mlip/key_storage/key_storage_factory.h b/include/tadah/mlip/key_storage/key_storage_factory.h new file mode 100644 index 0000000000000000000000000000000000000000..f1572bcdbbea4eff6af84ed7d6d5d60b6d7d95c0 --- /dev/null +++ b/include/tadah/mlip/key_storage/key_storage_factory.h @@ -0,0 +1,40 @@ +/** + * @file KeyStorageFactory.h + * @brief Factory for constructing complex IKeyStorage objects or single backends. + */ + +#ifndef KEYSTORAGEFACTORY_H +#define KEYSTORAGEFACTORY_H + +#include <memory> +#include <string> +#include <tadah/mlip/key_storage/ikey_storage.h> + +/** + * @class KeyStorageFactory + * @brief Produces aggregated or single IKeyStorage instances. + */ +class KeyStorageFactory +{ +public: + /** + * @brief Creates a fallback storage that checks environment first, then file, etc. + * @return A unique_ptr to IKeyStorage implementing the fallback strategy. + */ + static std::unique_ptr<IKeyStorage> createEnvThenFile(); + + /** + * @brief Creates a plain-text-only storage instance (no environment checks). + * @return A unique_ptr to IKeyStorage that uses plain-text only. + */ + static std::unique_ptr<IKeyStorage> createPlainTextOnly(); + + /** + * @brief Creates an environment-only storage instance (no file fallback). + * @return A unique_ptr to IKeyStorage that uses environment only. + */ + static std::unique_ptr<IKeyStorage> createEnvironmentOnly(); +}; + +#endif // KEYSTORAGEFACTORY_H + diff --git a/include/tadah/mlip/key_storage/plain_text_key_storage.h b/include/tadah/mlip/key_storage/plain_text_key_storage.h new file mode 100644 index 0000000000000000000000000000000000000000..c71a43eb88fa1c7aae5722fc4feafdfb7e9fe30c --- /dev/null +++ b/include/tadah/mlip/key_storage/plain_text_key_storage.h @@ -0,0 +1,60 @@ +/** + * @file PlainTextKeyStorage.h + * @brief Implementation of IKeyStorage that reads/writes plain-text keys in ~/.config/tadah/keys. + */ + +#ifndef PLAINTEXTKEYSTORAGE_H +#define PLAINTEXTKEYSTORAGE_H + +#include <tadah/mlip/key_storage/ikey_storage.h> +#include <vector> + +/** + * @class PlainTextKeyStorage + * @brief Handles storage of keys in a plain-text file. + * + * The file is located in "~/.config/tadah/keys" if possible, otherwise "." + * is used as a fallback. + * + * Format of each line: KEY_NAME value + * Example: MY_REST_API_KEY abc123 + * + * No encryption is performed in this implementation. + */ +class PlainTextKeyStorage : public IKeyStorage +{ +public: + /** + * @brief Constructor ensures the directory is set up. + */ + PlainTextKeyStorage(); + + /** + * @brief Retrieves @p keyName from the keys file (does not check environment). + * @param keyName The name of the key to look up. + * @return The value if found, otherwise empty. + */ + std::string retrieveKey(const std::string& keyName) override; + + /** + * @brief Writes or updates a line in the keys file, ignoring environment variables. + * @param keyName The name of the key. + * @param keyValue The value to associate with the key. + * @return true on successful file write, false on error. + */ + bool storeKey(const std::string& keyName, const std::string& keyValue) override; + +private: + std::string m_configDir; ///< Directory path used for storing the "keys" file. + + std::string prepareConfigDirectory(); + std::string getKeysFilePath() const; + + // Helpers for file read/write + std::string readKeyFromFile(const std::string& keyName); + std::vector<std::pair<std::string, std::string>> loadAllKeys(); + bool writeAllKeys(const std::vector<std::pair<std::string, std::string>>& kvPairs); +}; + +#endif // PLAINTEXTKEYSTORAGE_H + diff --git a/include/tadah/mlip/structure_readers/castep_cell_reader.h b/include/tadah/mlip/structure_readers/castep_cell_reader.h index d494f3034601773048cf35f500373ed38fc985e3..f75b38c39c5d188e6e0f225e20a10928c6565dc0 100644 --- a/include/tadah/mlip/structure_readers/castep_cell_reader.h +++ b/include/tadah/mlip/structure_readers/castep_cell_reader.h @@ -62,7 +62,7 @@ private: * @brief Converts atomic fractional coordinates (fx, fy, fz) to absolute in Å * using the current m_structure.cell matrix. */ - void applyFractional(double fx, double fy, double fz, double &cx, double &cy, + void fracToAbs(double fx, double fy, double fz, double &cx, double &cy, double &cz); /** diff --git a/src/castep_cell_reader.cpp b/src/castep_cell_reader.cpp index fb09541490e45fd2ae749c0a22fe5579ba2be4fc..4451c5fb13b893f4ee7eb365bf8586c7bd33d3b1 100644 --- a/src/castep_cell_reader.cpp +++ b/src/castep_cell_reader.cpp @@ -122,8 +122,12 @@ void CastepCellReader::parseCellContents(const std::string &contents) { // Now parse lines if in any recognized block if (inLatticeCart) { // read up to 3 lines for a,b,c - if (lcCount < 3) { + if (lcCount < 4) { auto toks = split_line(line); + if (toks.size() == 1) { + // skip optional units line + continue; + } if (toks.size() < 3) { throw std::runtime_error("CastepCellReader: LATTICE_CART line missing coords."); } @@ -147,7 +151,7 @@ void CastepCellReader::parseCellContents(const std::string &contents) { // e.g. "Si 0.0 0.0 0.0" auto toks = split_line(line); if (toks.size() < 4) { - // possibly a comment or empty line, skip + // skip empty lines which should not be there... continue; } @@ -160,7 +164,7 @@ void CastepCellReader::parseCellContents(const std::string &contents) { double fz = std::stod(toks[3]); double cx, cy, cz; - applyFractional(fx, fy, fz, cx, cy, cz); + fracToAbs(fx, fy, fz, cx, cy, cz); Atom a; // Copy base element data into 'a' @@ -172,9 +176,9 @@ void CastepCellReader::parseCellContents(const std::string &contents) { m_structure.atoms.push_back(a); } else if (inPosAbsBlock) { - // e.g. "O 1.2 3.4 5.6" auto toks = split_line(line); if (toks.size() < 4) { + // skip units line continue; } @@ -274,9 +278,9 @@ void CastepCellReader::parseLatticeABC(const std::vector<std::string> &lines) { } //-------------------------------------------------------------------- -// applyFractional +// fracToAbs //-------------------------------------------------------------------- -void CastepCellReader::applyFractional(double fx, double fy, double fz, +void CastepCellReader::fracToAbs(double fx, double fy, double fz, double &cx, double &cy, double &cz) { // r_cart = fx*a + fy*b + fz*c // a, b, c are rows of the 3×3 matrix in m_structure.cell diff --git a/src/environment_key_storage.cpp b/src/environment_key_storage.cpp new file mode 100644 index 0000000000000000000000000000000000000000..f28444d169e8e6331edde8207a54deea0656780d --- /dev/null +++ b/src/environment_key_storage.cpp @@ -0,0 +1,30 @@ +/** + * @file EnvironmentKeyStorage.cpp + * @brief Implementation of EnvironmentKeyStorage methods. + */ + +#include <tadah/mlip/key_storage/environment_key_storage.h> +#include <cstdlib> // getenv, setenv +#include <iostream> + +std::string EnvironmentKeyStorage::retrieveKey(const std::string& keyName) +{ + if (const char* val = std::getenv(keyName.c_str())) { + return std::string(val); + } + return ""; +} + +bool EnvironmentKeyStorage::storeKey(const std::string& keyName, const std::string& keyValue) +{ + // setenv updates this process environment only. + // If persistent environment changes are desired, + // the user would need to do that manually in shell profiles, etc. + if (setenv(keyName.c_str(), keyValue.c_str(), 1) == 0) { + return true; + } + + std::cerr << "Warning: setenv failed for key: " << keyName << "\n"; + return false; +} + diff --git a/src/fallback_key_storage.cpp b/src/fallback_key_storage.cpp new file mode 100644 index 0000000000000000000000000000000000000000..0a5a5c3ffc83e6a2972be2fcd580da63cb4b8996 --- /dev/null +++ b/src/fallback_key_storage.cpp @@ -0,0 +1,33 @@ +/** + * @file FallbackKeyStorage.cpp + * @brief Implementation of the aggregator for multiple storages in a fallback chain. + */ + +#include <tadah/mlip/key_storage/fallback_key_storage.h> + +FallbackKeyStorage::FallbackKeyStorage(std::vector<std::unique_ptr<IKeyStorage>> storages) + : m_storages(std::move(storages)) +{ +} + +std::string FallbackKeyStorage::retrieveKey(const std::string& keyName) +{ + // Check each storage in order, return the first non-empty result + for (auto &storage : m_storages) { + std::string val = storage->retrieveKey(keyName); + if (!val.empty()) { + return val; + } + } + return ""; // Not found in any storage +} + +bool FallbackKeyStorage::storeKey(const std::string& keyName, const std::string& keyValue) +{ + if (m_storages.empty()) { + return false; + } + // For simplicity: store in the last storage + return m_storages.back()->storeKey(keyName, keyValue); +} + diff --git a/src/key_manager.cpp b/src/key_manager.cpp new file mode 100644 index 0000000000000000000000000000000000000000..4fb5a0600c5fe6da928c02f83fa28edb246b6e5e --- /dev/null +++ b/src/key_manager.cpp @@ -0,0 +1,162 @@ +/** + * @file KeyManager.cpp + * @brief Implements a KeyManager that provides a four-option prompt + * if a requested key is missing. + */ +#include <tadah/mlip/key_storage/key_manager.h> +#include <tadah/mlip/key_storage/key_storage_factory.h> + +#include <iostream> +#include <string> +#include <cstdlib> // setenv on Unix-likes + +std::string KeyManager::getServiceKey(ServiceType service, bool allowPrompt) +{ + // 1) Resolve which env/config key to look for + const std::string keyName = resolveKeyName(service); + + // 2) Acquire fallback storage that checks environment first, then the file + auto storage = KeyStorageFactory::createEnvThenFile(); + + // 3) Attempt to retrieve the key + std::string val = storage->retrieveKey(keyName); + if (!val.empty() || !allowPrompt) { + // Key found, or prompting disallowed => just return + return val; + } + + // Key is missing and we can prompt -> show the 4-option menu + std::cout << "\nNo key found for " << getServiceFriendlyName(service) + << " (" << keyName << ").\n" + << "To obtain a key, visit the official website for instructions.\n\n"; + + int choice = promptUserChoice(); + switch (choice) { + case 1: // Store in config file + { + std::cout << "Please enter the key now (Ctrl+C to cancel):\n> "; + std::getline(std::cin, val); + if (val.empty()) { + std::cerr << "No key entered, returning empty.\n"; + return ""; + } + bool storeOk = storage->storeKey(keyName, val); + if (!storeOk) { + std::cerr << "Failed to store key in config.\n"; + } else { + std::cout << "[Info] Key stored in config file (~/.config/tadah/keys).\n"; + } + return val; + } + + case 2: // Temporarily set environment var for this run + { + std::cout << "Please enter the key now (Ctrl+C to cancel):\n> "; + std::getline(std::cin, val); + if (val.empty()) { + std::cerr << "No key entered, returning empty.\n"; + return ""; + } + setEnvCurrentProcess(keyName, val); + std::cout << "\n[Info] Key applied to this process only.\n" + << "It will not be seen by other programs or future runs.\n"; + return val; + } + + case 3: // Show instructions for exporting in this shell + { + std::cout << "This option does not store or set the key immediately.\n" + << "It only shows how to export the key in your terminal.\n" + << "Please copy the example commands below.\n\n"; + // We can guess a random placeholder or let user type it + // For a real flow, you might also prompt for the key + std::cout << "Please enter the key (Ctrl+C to cancel):\n> "; + std::getline(std::cin, val); + if (val.empty()) { + std::cerr << "No key entered, returning empty.\n"; + return ""; + } + showShellExportInstructions(keyName, val); + return val; + } + + default: // 4: Abort + std::cerr << "Aborting. Key remains missing.\n"; + return ""; + } +} + +std::string KeyManager::resolveKeyName(ServiceType service) +{ + switch (service) { + case ServiceType::MP: return "MP_REST_API_KEY"; + case ServiceType::COD: return "COD_REST_API_KEY"; + case ServiceType::AFLOW: return "AFLOW_REST_API_KEY"; + } + return "UNKNOWN_SERVICE_KEY"; +} + +std::string KeyManager::getServiceFriendlyName(ServiceType service) +{ + switch (service) { + case ServiceType::MP: return "Materials Project"; + case ServiceType::COD: return "Crystallography Open Database (COD)"; + case ServiceType::AFLOW: return "AFLOW Project"; + } + return "Unknown Service"; +} + +int KeyManager::promptUserChoice() +{ + // Four options now + std::cout << "Please select an option:\n" + << "[1] Store the key in a config file (persistent)\n" + << "[2] Temporarily set environment variable for THIS run only\n" + << "[3] View instructions for exporting in your shell (useful across multiple runs)\n" + << "[4] Abort (do nothing)\n" + << "\nEnter choice [1..4]: "; + + std::string line; + std::getline(std::cin, line); + if (line.empty()) { + return 4; // default to abort if user just hits Enter + } + char c = line[0]; + if (c == '1') return 1; + if (c == '2') return 2; + if (c == '3') return 3; + return 4; // invalid or '4' => abort +} + +void KeyManager::setEnvCurrentProcess(const std::string &keyName, const std::string &keyValue) +{ +#if defined(_WIN32) + // Windows example: _putenv_s + // For demonstration; might need <cstdlib> or <corecrt.h> + // _putenv_s(keyName.c_str(), keyValue.c_str()); + // or use SetEnvironmentVariable (WinAPI call) + std::cout << "[Warning] Setting environment variables at runtime on Windows " + "might require different API calls.\n"; +#else + // Unix-like approach + if (setenv(keyName.c_str(), keyValue.c_str(), 1) != 0) { + std::cerr << "Could not set environment variable in this process.\n"; + } +#endif +} + +void KeyManager::showShellExportInstructions(const std::string &keyName, const std::string &keyValue) +{ + std::cout << "------------------------------------------------------------\n" + << "To export this key for your current TERMINAL session:\n\n" + << " export " << keyName << "=\"" << keyValue << "\"\n\n" + << "After this, any program run in the same terminal will see the key.\n" + << "------------------------------------------------------------\n" + << "To make this permanent for ALL future terminal sessions in bash or zsh,\n" + << "add the same line to your ~/.bashrc or ~/.zshrc file:\n\n" + << " echo 'export " << keyName << "=\"" << keyValue << "\"' >> ~/.bashrc\n" + << "------------------------------------------------------------\n" + << "Once added, reload your shell or open a new terminal.\n" + << "Then re-run this program, and it will see that environment variable.\n\n"; +} + diff --git a/src/key_storage_factory.cpp b/src/key_storage_factory.cpp new file mode 100644 index 0000000000000000000000000000000000000000..a250c87161c9f0d563d87f6afe2b4069a9d904d7 --- /dev/null +++ b/src/key_storage_factory.cpp @@ -0,0 +1,30 @@ +/** + * @file KeyStorageFactory.cpp + * @brief Implementation of the KeyStorageFactory class. + */ + +#include <tadah/mlip/key_storage/environment_key_storage.h> +#include <tadah/mlip/key_storage/plain_text_key_storage.h> +#include <tadah/mlip/key_storage/fallback_key_storage.h> +#include <tadah/mlip/key_storage/key_storage_factory.h> +#include <vector> + +std::unique_ptr<IKeyStorage> KeyStorageFactory::createEnvThenFile() +{ + // Define storages in order: environment first, then file + std::vector<std::unique_ptr<IKeyStorage>> storages; + storages.push_back(std::make_unique<EnvironmentKeyStorage>()); + storages.push_back(std::make_unique<PlainTextKeyStorage>()); + return std::make_unique<FallbackKeyStorage>(std::move(storages)); +} + +std::unique_ptr<IKeyStorage> KeyStorageFactory::createPlainTextOnly() +{ + return std::make_unique<PlainTextKeyStorage>(); +} + +std::unique_ptr<IKeyStorage> KeyStorageFactory::createEnvironmentOnly() +{ + return std::make_unique<EnvironmentKeyStorage>(); +} + diff --git a/src/materials_project_reader.cpp b/src/materials_project_reader.cpp index dc688bd4b0af0b1e24854e8cdd44118018f72b66..74f6e52ea4fb0b19924f2838d9c9e0b603cfffea 100644 --- a/src/materials_project_reader.cpp +++ b/src/materials_project_reader.cpp @@ -1,122 +1,242 @@ -#include <sstream> -#include <stdexcept> -#include <tadah/mlip/structure_readers/materials_project_reader.h> +/** + MaterialsProjectReader.cpp + Implements functionality declared in MaterialsProjectReader.h. + + Example usage: + -------------------------------------------------------------------- + // 1) The request sets multiple query parameters: + // material_ids=mp-35 + // deprecated=false + // _per_page=100 + // _skip=0 + // _limit=100 + // _all_fields=true + // license=BY-NC + // + // 2) The request sets the header: + // X-API-KEY: <some-api-key> + // + // 3) The server is expected to return JSON data with an array + // under "data", each item a "MaterialsDoc" from which we parse + // the "structure" field's lattice and sites. + // + // 4) This code is integrated with the rest of the codebase by + // using the MaterialsProjectReader constructor to store the + // API key. The read() method then forms the correct URL + // with the additional query parameters and a custom header. +*/ -#include <curl/curl.h> // libcurl -#include <nlohmann/json.hpp> // nlohmann JSON +#include <tadah/mlip/structure_readers/materials_project_reader.h> +#include <curl/curl.h> +#include <nlohmann/json.hpp> +#include <stdexcept> +#include <sstream> +#include <iostream> -static Element parse_element_mp(const std::string &elemName); +// parse_element_mp helps parse site labels or species info to retrieve +// an Element from a known PeriodicTable utility. +static Element parse_element_mp(const std::string &elemName) { + return PeriodicTable().find_by_symbol(elemName); +} +// Constructor: stores user-provided Materials Project API key. MaterialsProjectReader::MaterialsProjectReader(const std::string &apiKey) - : m_apiKey(apiKey) {} + : m_apiKey(apiKey) { +} +// read fetches and parses structure data for a given mpID (e.g. "mp-35"). void MaterialsProjectReader::read(const std::string &mpID) { fetchAndParseMP(mpID); - m_structure.label = "MaterialsProject ID: " + mpID; } -Structure MaterialsProjectReader::getStructure() const { return m_structure; } +// getStructure returns the last structure retrieved. +Structure MaterialsProjectReader::getStructure() const { + return m_structure; +} +// fetchAndParseMP forms a query URL that includes the user example query params, +// calls httpGet with a relevant header, then parses the JSON in parseMpJson. void MaterialsProjectReader::fetchAndParseMP(const std::string &mpID) { - std::string url = "https://materialsproject.org/rest/v2/materials/"; - url += mpID + "/structure?API_KEY=" + m_apiKey; - + const std::string url = + "https://api.materialsproject.org/materials/core/" + "?material_ids=" + + mpID + + "&deprecated=false" + "&_per_page=100" + "&_skip=0" + "&_limit=100" + "&_all_fields=true" + "&license=BY-NC"; + + // Execute an HTTP GET request via cURL std::string response = httpGet(url); + + // Parse the JSON response parseMpJson(response); } +// This callback accumulates data in a std::string. +size_t writeCallback(char* ptr, size_t size, size_t nmemb, void* userdata) { + auto* response = static_cast<std::string*>(userdata); + size_t totalBytes = size * nmemb; + response->append(ptr, totalBytes); + return totalBytes; +} +// httpGet uses cURL to perform an HTTP GET on `url`, including +// the X-API-KEY header matching the Materials Project specification. std::string MaterialsProjectReader::httpGet(const std::string &url) { - CURL *curl = curl_easy_init(); + + // cURL initialization + CURL* curl = curl_easy_init(); if (!curl) { - throw std::runtime_error( - "MaterialsProjectReader::httpGet: Failed to init libcurl"); + std::cerr << "Error: Failed to init cURL\n"; } - auto writeCallback = [](char *ptr, size_t size, size_t nmemb, - void *userdata) -> size_t { - std::string *resp = static_cast<std::string *>(userdata); - size_t totalSize = size * nmemb; - resp->append(ptr, totalSize); - return totalSize; - }; - + // The server response will be stored here. std::string response; + + // Prepare custom headers: + struct curl_slist* headers = nullptr; + { + std::stringstream ss; + ss << "X-API-KEY: " << m_apiKey; + headers = curl_slist_append(headers, ss.str().c_str()); + } + headers = curl_slist_append(headers, "accept: application/json"); + + // cURL basic configuration: curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + + // User-Agent string is required by the Materials Project API + // to identify the client. Otherwise, the request will be rejected. + curl_easy_setopt(curl, CURLOPT_USERAGENT, + "Tadah! (https://tadah.readthedocs.io)"); + + // The write callback to collect the HTTP response data. curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); + + // Follow redirects if needed curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + // Perform the HTTP GET CURLcode res = curl_easy_perform(curl); if (res != CURLE_OK) { + std::cerr << "[DEBUG] cURL perform failed with error code: " << res << std::endl; + // Cleanup on error + curl_slist_free_all(headers); curl_easy_cleanup(curl); throw std::runtime_error( - std::string("httpGet: curl_easy_perform() failed: ") + - curl_easy_strerror(res)); + std::string("httpGet: curl_easy_perform() failed: ") + + curl_easy_strerror(res)); } + + // Cleanup + curl_slist_free_all(headers); curl_easy_cleanup(curl); + return response; } +// parseMpJson processes the JSON from the Materials Project +// "materials/core" endpoint. "data" is an array of results. The first +// result is a "MaterialsDoc" containing a "structure" sub-field. The +// "structure" sub-field is a JSON representation of a Pymatgen structure +// that includes a "lattice.matrix" and a "sites" list. Lattice vectors +// and atomic positions are then stored in m_structure. void MaterialsProjectReader::parseMpJson(const std::string &jsonContent) { using json = nlohmann::json; json root = json::parse(jsonContent); - if (!root.contains("response") || !root["response"].is_array() || - root["response"].empty()) { - throw std::runtime_error("parseMpJson: No response array in JSON."); + if (!root.contains("data") || !root["data"].is_array() || + root["data"].empty()) { + throw std::runtime_error("parseMpJson: Missing or empty 'data' array."); } - auto data = root["response"][0]; - if (!data.contains("lattice") || !data["lattice"].contains("matrix")) { - throw std::runtime_error("parseMpJson: Missing lattice.matrix"); + auto doc = root["data"][0]; + if (!doc.contains("structure")) { + throw std::runtime_error("parseMpJson: 'structure' not found."); } - auto mat = data["lattice"]["matrix"]; - if (!mat.is_array() || mat.size() != 3) { - throw std::runtime_error("parseMpJson: matrix is not 3x3"); + auto structureJson = doc["structure"]; + if (!structureJson.contains("lattice") || + !structureJson["lattice"].contains("matrix")) { + throw std::runtime_error("parseMpJson: Missing 'lattice.matrix'."); } + // Build a label for the structure + auto symbol = to_string(doc["symmetry"]["symbol"]); + auto volume = to_string(doc["volume"]); + auto density = to_string(doc["density"]); + auto elements = to_string(doc["elements"]); + auto mid = to_string(doc["material_id"]); + m_structure.label = "MaterialsProject ID: " + mid+ " | " + + "Symmetry: " + symbol+ " | " + + "Volume: " + volume+ " | " + + "Density: " + density+ " | " + + "Elements: " + elements+ " | "; + + // Extract the 3x3 lattice matrix + auto mat = structureJson["lattice"]["matrix"]; + if (!mat.is_array() || mat.size() != 3) { + throw std::runtime_error("parseMpJson: 'matrix' must be array of length 3."); + } for (int i = 0; i < 3; i++) { if (!mat[i].is_array() || mat[i].size() != 3) { - throw std::runtime_error("parseMpJson: matrix row is not size 3"); + throw std::runtime_error("parseMpJson: matrix row must be length 3."); } m_structure.cell(i, 0) = mat[i][0].get<double>(); m_structure.cell(i, 1) = mat[i][1].get<double>(); m_structure.cell(i, 2) = mat[i][2].get<double>(); } - if (!data.contains("sites") || !data["sites"].is_array()) { - throw std::runtime_error("parseMpJson: No sites array"); + // 'sites' array: each site has fractional coordinates "abc" + if (!structureJson.contains("sites") || !structureJson["sites"].is_array()) { + throw std::runtime_error("parseMpJson: 'sites' is missing or not an array."); } - auto sites = data["sites"]; + auto sites = structureJson["sites"]; + for (auto &site : sites) { if (!site.contains("abc") || !site["abc"].is_array() || site["abc"].size() != 3) { - throw std::runtime_error("parseMpJson: site missing abc array"); + throw std::runtime_error("parseMpJson: Site missing valid 'abc'."); } double fx = site["abc"][0].get<double>(); double fy = site["abc"][1].get<double>(); double fz = site["abc"][2].get<double>(); - // fractional -> cart - double x = fx * m_structure.cell(0, 0) + fy * m_structure.cell(1, 0) + + // Convert fractional -> cart + double x = fx * m_structure.cell(0, 0) + + fy * m_structure.cell(1, 0) + fz * m_structure.cell(2, 0); - double y = fx * m_structure.cell(0, 1) + fy * m_structure.cell(1, 1) + + double y = fx * m_structure.cell(0, 1) + + fy * m_structure.cell(1, 1) + fz * m_structure.cell(2, 1); - double z = fx * m_structure.cell(0, 2) + fy * m_structure.cell(1, 2) + + double z = fx * m_structure.cell(0, 2) + + fy * m_structure.cell(1, 2) + fz * m_structure.cell(2, 2); + // The element can be found in either 'label' or 'species[0].element' + std::string elemSymbol; + if (site.contains("label") && site["label"].is_string()) { + elemSymbol = site["label"].get<std::string>(); + } else if (site.contains("species") && site["species"].is_array() && + !site["species"].empty() && + site["species"][0].contains("element")) { + elemSymbol = site["species"][0]["element"].get<std::string>(); + } else { + throw std::runtime_error( + "parseMpJson: No recognized element data in site."); + } + Atom a; - std::string lbl = site["label"].get<std::string>(); - Element e = parse_element_mp(lbl); + Element e = parse_element_mp(elemSymbol); static_cast<Element &>(a) = e; a.position[0] = x; a.position[1] = y; a.position[2] = z; + m_structure.atoms.push_back(a); } } - -static Element parse_element_mp(const std::string &elemName) { - return PeriodicTable().find_by_symbol(elemName); -} diff --git a/src/nomad_reader.cpp b/src/nomad_reader.cpp new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/plain_text_key_storage.cpp b/src/plain_text_key_storage.cpp new file mode 100644 index 0000000000000000000000000000000000000000..014ba23191d0275bbb26c6de0bc8c39fe9235dea --- /dev/null +++ b/src/plain_text_key_storage.cpp @@ -0,0 +1,140 @@ +/** + * @file PlainTextKeyStorage.cpp + * @brief Implementation file for PlainTextKeyStorage class. + */ + +#include <tadah/mlip/key_storage/plain_text_key_storage.h> +#include <iostream> +#include <fstream> +#include <string> +#include <cstdlib> +#include <sys/stat.h> +#include <sys/types.h> +#include <unistd.h> + +PlainTextKeyStorage::PlainTextKeyStorage() +{ + m_configDir = prepareConfigDirectory(); +} + +std::string PlainTextKeyStorage::retrieveKey(const std::string& keyName) +{ + return readKeyFromFile(keyName); +} + +bool PlainTextKeyStorage::storeKey(const std::string& keyName, const std::string& keyValue) +{ + auto pairs = loadAllKeys(); + bool updated = false; + + // Update existing or append + for (auto &kv : pairs) { + if (kv.first == keyName) { + kv.second = keyValue; // update + updated = true; + break; + } + } + if (!updated) { + pairs.emplace_back(keyName, keyValue); + } + + return writeAllKeys(pairs); +} + +std::string PlainTextKeyStorage::prepareConfigDirectory() +{ + const char* homeDir = std::getenv("HOME"); + if (!homeDir) { + std::cerr << "Warning: HOME not set; using current directory for keys file.\n"; + return "."; + } + + std::string configDir = std::string(homeDir) + "/.config/tadah"; + struct stat sb; + if (stat(configDir.c_str(), &sb) != 0) { + // Directory does not exist -> mkdir + if (mkdir(configDir.c_str(), 0700) != 0) { + std::cerr << "Error: Could not create config directory: " << configDir + << "\nFalling back to current directory.\n"; + return "."; + } + } else if (!S_ISDIR(sb.st_mode)) { + // Path exists but is not a directory + std::cerr << "Error: " << configDir << " is not a directory.\n" + << "Falling back to current directory.\n"; + return "."; + } + + return configDir; +} + +std::string PlainTextKeyStorage::getKeysFilePath() const +{ + return m_configDir + "/keys"; +} + +std::string PlainTextKeyStorage::readKeyFromFile(const std::string& keyName) +{ + std::ifstream fileIn(getKeysFilePath()); + if (!fileIn.good()) { + return ""; + } + + std::string line; + while (std::getline(fileIn, line)) { + if (line.empty()) continue; + + auto spacePos = line.find(' '); + if (spacePos == std::string::npos) { + continue; // skip malformed + } + + std::string k = line.substr(0, spacePos); + std::string v = line.substr(spacePos + 1); + + if (k == keyName) { + return v; + } + } + + return ""; +} + +std::vector<std::pair<std::string, std::string>> PlainTextKeyStorage::loadAllKeys() +{ + std::vector<std::pair<std::string, std::string>> kvPairs; + std::ifstream fileIn(getKeysFilePath()); + if (!fileIn.good()) { + return kvPairs; + } + + std::string line; + while (std::getline(fileIn, line)) { + if (line.empty()) continue; + auto spacePos = line.find(' '); + if (spacePos == std::string::npos) { + continue; + } + std::string k = line.substr(0, spacePos); + std::string v = line.substr(spacePos + 1); + kvPairs.emplace_back(k, v); + } + + return kvPairs; +} + +bool PlainTextKeyStorage::writeAllKeys(const std::vector<std::pair<std::string, std::string>>& kvPairs) +{ + std::ofstream fileOut(getKeysFilePath(), std::ios::trunc); + if (!fileOut.good()) { + std::cerr << "Could not write to file: " << getKeysFilePath() << "\n"; + return false; + } + + for (const auto &kv : kvPairs) { + fileOut << kv.first << " " << kv.second << "\n"; + } + return true; +} + diff --git a/src/structure_reader_selector.cpp b/src/structure_reader_selector.cpp index 684d2ce3c92aeb1228fe30c15c18d5ccdea2dd4a..44ecbbf8f5bf1cdd9d8997c83d09913dfeccc6c6 100644 --- a/src/structure_reader_selector.cpp +++ b/src/structure_reader_selector.cpp @@ -1,114 +1,118 @@ -#include <tadah/mlip/structure_readers/structure_reader_selector.h> +#include <tadah/mlip/structure_readers/castep_cell_reader.h> #include <tadah/mlip/structure_readers/cif_reader.h> +#include <tadah/mlip/structure_readers/structure_reader_selector.h> #include <tadah/mlip/structure_readers/vasp_poscar_reader.h> -#include <tadah/mlip/structure_readers/castep_cell_reader.h> /*#include <tadah/mlip/structure_readers/extended_xyz_reader.h>*/ /*#include <tadah/mlip/structure_readers/xsf_reader.h>*/ #include <tadah/mlip/structure_readers/materials_project_reader.h> /*#include <tadah/mlip/structure_readers/cod_reader.h>*/ /*#include <tadah/mlip/structure_readers/aflow_reader.h>*/ /*#include <tadah/mlip/structure_readers/nomad_reader.h>*/ +#include <tadah/mlip/key_storage/key_manager.h> -#include <fstream> #include <algorithm> #include <cctype> +#include <fstream> #include <stdexcept> -std::unique_ptr<StructureReader> StructureReaderSelector::getReader(const std::string& pathOrId) -{ - std::cout << "Reading structure from: " << pathOrId << std::endl; - std::string format = guessFormat(pathOrId); - std::cout << "Format: " << format << std::endl; +std::unique_ptr<StructureReader> +StructureReaderSelector::getReader(const std::string &pathOrId) { + std::string format = guessFormat(pathOrId); - if (format == "MP") { - return std::make_unique<MaterialsProjectReader>("MY_MP_API_KEY"); // Provide your real key - } - else if (format == "CIF") { - std::cout << "CIF Reader Selected" << std::endl; - return std::make_unique<CifReader>(); - } - else if (format == "POSCAR") { - return std::make_unique<VaspPoscarReader>(); - } - else if (format == "CELL") { - return std::make_unique<CastepCellReader>(); - } - /*else if (format == "XYZ") {*/ - /* return std::make_unique<ExtendedXyzReader>();*/ - /*}*/ - /*else if (format == "XSF") {*/ - /* return std::make_unique<XsfReader>();*/ - /*}*/ - /*else if (format == "COD") {*/ - /* return std::make_unique<CODReader>();*/ - /*}*/ - /*else if (format == "AFLOW") {*/ - /* return std::make_unique<AflowReader>();*/ - /*}*/ - /*else if (format == "NOMAD") {*/ - /* return std::make_unique<NomadReader>();*/ - /*}*/ - else { - throw std::runtime_error("StructureReaderSelector::getReader - Unknown format for " + pathOrId); + if (format == "MP") { + std::string mpKey = + KeyManager::getServiceKey(ServiceType::MP, /*allowPrompt=*/true); + if (mpKey.empty()) { + throw std::runtime_error( + "No valid Materials Project key provided. Cannot continue."); } + return std::make_unique<MaterialsProjectReader>(mpKey); + } else if (format == "CIF") { + return std::make_unique<CifReader>(); + } else if (format == "POSCAR") { + return std::make_unique<VaspPoscarReader>(); + } else if (format == "CELL") { + return std::make_unique<CastepCellReader>(); + } + /*else if (format == "XYZ") {*/ + /* return std::make_unique<ExtendedXyzReader>();*/ + /*}*/ + /*else if (format == "XSF") {*/ + /* return std::make_unique<XsfReader>();*/ + /*}*/ + /*else if (format == "COD") {*/ + /* return std::make_unique<CODReader>();*/ + /*}*/ + /*else if (format == "AFLOW") {*/ + /* return std::make_unique<AflowReader>();*/ + /*}*/ + /*else if (format == "NOMAD") {*/ + /* return std::make_unique<NomadReader>();*/ + /*}*/ + else { + throw std::runtime_error( + "StructureReaderSelector::getReader - Unknown format for " + pathOrId); + } } -std::string StructureReaderSelector::guessFormat(const std::string& pathOrId) -{ - // Check known online ID patterns: - if (pathOrId.rfind("mp-", 0) == 0) { - return "MP"; - } - if (pathOrId.rfind("cod-", 0) == 0) { - return "COD"; - } - if (pathOrId.rfind("aflow-", 0) == 0) { - return "AFLOW"; - } - if (pathOrId.rfind("nomad-", 0) == 0) { - return "NOMAD"; - } +std::string StructureReaderSelector::guessFormat(const std::string &pathOrId) { + // Check known online ID patterns: + if (pathOrId.rfind("mp-", 0) == 0) { + return "MP"; + } + if (pathOrId.rfind("cod-", 0) == 0) { + return "COD"; + } + if (pathOrId.rfind("aflow-", 0) == 0) { + return "AFLOW"; + } + if (pathOrId.rfind("nomad-", 0) == 0) { + return "NOMAD"; + } - // Check file extension: - auto dotPos = pathOrId.find_last_of('.'); - if (dotPos != std::string::npos) { - std::string ext = pathOrId.substr(dotPos + 1); - std::transform(ext.begin(), ext.end(), ext.begin(), - [](unsigned char c){ return std::tolower(c); }); + // Check file extension: + auto dotPos = pathOrId.find_last_of('.'); + if (dotPos != std::string::npos) { + std::string ext = pathOrId.substr(dotPos + 1); + std::transform(ext.begin(), ext.end(), ext.begin(), + [](unsigned char c) { return std::tolower(c); }); - if (ext == "cif") return "CIF"; - if (ext == "cell") return "CELL"; - if (ext == "xsf") return "XSF"; - if (ext == "xyz") return "XYZ"; - // Some VASP files may not have an extension - } + if (ext == "cif") + return "CIF"; + if (ext == "cell") + return "CELL"; + if (ext == "xsf") + return "XSF"; + if (ext == "xyz") + return "XYZ"; + // Some VASP files may not have an extension + } - return guessFormatFromContent(pathOrId); + return guessFormatFromContent(pathOrId); } -std::string StructureReaderSelector::guessFormatFromContent(const std::string& filePath) -{ - std::ifstream ifs(filePath); - if (!ifs.is_open()) { - // If can't open, guess POSCAR (commonly no extension). - return "POSCAR"; - } +std::string +StructureReaderSelector::guessFormatFromContent(const std::string &filePath) { + std::ifstream ifs(filePath); + if (!ifs.is_open()) { + // If can't open, guess POSCAR (commonly no extension). + return "POSCAR"; + } - std::string line; - if (std::getline(ifs, line)) { - // Some naive checks - if (line.find("lattice_vector") != std::string::npos) { - return "XSF"; - } - if (line.find("CELL_PARAMETERS") != std::string::npos) { - return "CIF"; - } - if (line.find("%BLOCK LATTICE_") != std::string::npos) { - return "CELL"; - } + std::string line; + if (std::getline(ifs, line)) { + // Some naive checks + if (line.find("lattice_vector") != std::string::npos) { + return "XSF"; + } + if (line.find("CELL_PARAMETERS") != std::string::npos) { + return "CIF"; + } + if (line.find("%BLOCK LATTICE_") != std::string::npos) { + return "CELL"; } + } - // fallback - return "POSCAR"; + // fallback + return "POSCAR"; } -