Skip to content

Commit

Permalink
[updatePrice, viewInventory] Added support for using GET STOCK FILE t…
Browse files Browse the repository at this point in the history
…o retrive stock - less requests

* more systematic synonyms handling in MKMMetaCard
* added basic support for Yu-Gi-Oh's FirstEd
* All CSV processing code is now in a separate file
  • Loading branch information
tomasjanak committed Nov 14, 2020
1 parent caed37d commit 5939d98
Show file tree
Hide file tree
Showing 9 changed files with 445 additions and 265 deletions.
77 changes: 24 additions & 53 deletions MKMTool/MKMBot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -428,7 +428,7 @@ public void generatePrices(List<MKMMetaCard> cardList, bool useMyStock)
MainView.Instance.LogMainWindow("Found myStock.csv, parsing minimal prices...");
try
{
DataTable stock = MKMDbManager.ConvertCSVtoDataTable(@".//myStock.csv");
DataTable stock = MKMCsvUtils.ConvertCSVtoDataTable(@".//myStock.csv");
if (stock.Columns.Contains(MCAttribute.MinPrice))
{
foreach (DataRow dr in stock.Rows)
Expand Down Expand Up @@ -460,72 +460,42 @@ public void updatePrices()
{
MainView.Instance.LogMainWindow("Setting price according to lowest price is very risky - specify limits for maximal price change first!");
return;
}

#if (DEBUG)
var debugCounter = 0;
#endif
List<XmlNode> articles = new List<XmlNode>();
string sRequestXML = "";
XmlNodeList result;
var start = 1;
// load file with lowest prices
Dictionary<string, List<MKMMetaCard>> myStock = LoadMyStock();
}
List<MKMMetaCard> articles;
try
{
do
{
var doc = MKMInteract.RequestHelper.readStock(start);

result = doc.GetElementsByTagName("article");
foreach (XmlNode article in result)
{
articles.Add(article);
}
start += result.Count;
} while (result.Count == 100);
articles = MKMInteract.RequestHelper.getAllStockSingles(MainView.Instance.Config.UseStockGetFile);
}
catch (Exception error)
{
MKMHelpers.LogError("reading own stock, cannot continue price update", error.Message, true);
return;
}
// load file with lowest prices
Dictionary<string, List<MKMMetaCard>> myStock = LoadMyStock();

MainView.Instance.LogMainWindow("Updating Prices...");
int putCounter = 0;
foreach (XmlNode article in articles)
string sRequestXML = "";
foreach (MKMMetaCard MKMCard in articles)
{
#if (DEBUG)
debugCounter++;
if (debugCounter > 3)
{
MainView.Instance.logMainWindow("DEBUG MODE - EXITING AFTER 3\n");
break;
}
#endif
// according to the API documentation, "The 'condition' key is only returned for single cards. "
// -> check if condition exists to see if this is a single card or something else
if (article["condition"] != null && article["idArticle"].InnerText != null && article["price"].InnerText != null)
XmlNodeList similarItems = getSimilarItems(MKMCard);
if (similarItems != null)
{
MKMMetaCard MKMCard = new MKMMetaCard(article);
XmlNodeList similarItems = getSimilarItems(MKMCard);
if (similarItems != null)
appraiseArticle(MKMCard, similarItems, myStock);
string newPrice = MKMCard.GetAttribute(MCAttribute.MKMToolPrice);
if (newPrice != "")
{
appraiseArticle(MKMCard, similarItems, myStock);
string newPrice = MKMCard.GetAttribute(MCAttribute.MKMToolPrice);
if (newPrice != "")
sRequestXML += MKMInteract.RequestHelper.changeStockArticleBody(MKMCard, newPrice);
// max 100 articles is allowed to be part of a PUT call - if we are there, call it
if (putCounter > 98 && !settings.testMode)
{
sRequestXML += MKMInteract.RequestHelper.changeStockArticleBody(MKMCard, newPrice);
// max 100 articles is allowed to be part of a PUT call - if we are there, call it
if (putCounter > 98 && !settings.testMode)
{
MKMInteract.RequestHelper.SendStockUpdate(sRequestXML, "PUT");
putCounter = 0;
sRequestXML = "";
}
else
putCounter++;
MKMInteract.RequestHelper.SendStockUpdate(sRequestXML, "PUT");
putCounter = 0;
sRequestXML = "";
}
else
putCounter++;
}
}
}
Expand All @@ -542,7 +512,7 @@ public void updatePrices()
MainView.Instance.LogMainWindow("Done. No valid/meaningful price updates created.");
}

String timeStamp = GetTimestamp(DateTime.Now);
string timeStamp = GetTimestamp(DateTime.Now);

MainView.Instance.LogMainWindow("Last Run finished: " + timeStamp);
}
Expand All @@ -565,14 +535,15 @@ private XmlNodeList getSimilarItems(MKMMetaCard card, int maxNbItems = 150)
string isFoil = card.GetAttribute(MCAttribute.Foil);
string isSigned = card.GetAttribute(MCAttribute.Signed);
string isAltered = card.GetAttribute(MCAttribute.Altered);
string isFirstEd = card.GetAttribute(MCAttribute.FirstEd);
string articleName = card.GetAttribute(MCAttribute.Name);
try
{
sUrl = "https://api.cardmarket.com/ws/v2.0/articles/" + productID +
(languageID != "" ? "?idLanguage=" + card.GetAttribute(MCAttribute.LanguageID) : "") +
(condition != "" ? "&minCondition=" + condition : "") + (isFoil != "" ? "&isFoil=" + isFoil : "") +
(isSigned != "" ? "&isSigned=" + isSigned : "") + (isAltered != "" ? "&isAltered=" + isAltered : "") +
"&start=0&maxResults=" + maxNbItems;
(isFirstEd != "" ? "&isFirstEd=" + isFirstEd : "") + "&start=0&maxResults=" + maxNbItems;

return MKMInteract.RequestHelper.makeRequest(sUrl, "GET").GetElementsByTagName("article");
}
Expand Down
191 changes: 191 additions & 0 deletions MKMTool/MKMCsvUtils.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Linq;
using static MKMTool.MKMHelpers;

namespace MKMTool
{
/// <summary>
/// Helper class for processing CSV files.
/// </summary>
class MKMCsvUtils
{

/// <summary>
/// Parses a row in a CSV file.
/// </summary>
/// <param name="rowToParse">The row from a CSV file. Each value can be enclosed by double quotes (i.e. the character ")
/// and if a double quote is part of the value, it is escaped by another double quote.
/// If a field contains an even number of double quotes and nothing else, it will be considered as not enclosed, so a field that contains """""" will
/// be parsed as """. The only exception is when there are exactly two double quotes, in that case it will be considered an enclosed empty string.
/// This is done as in practice, excel and similar might enclose the empty fields and the user probably wants to keep them empty.</param>
/// <param name="separator">The character used as separator between columns.</param>
/// <returns>A list of the individual parsed values in the order they appear on the row. All enclosing quotes are trimmed
/// from the value and all escape characters are removed.</returns>
private static List<string> parseCSVRow(string rowToParse, char separator)
{
List<string> ret = new List<string>();
string[] split = rowToParse.Split(separator);

for (int i = 0; i < split.Length; i++)
{
string columnValue = split[i];
// we need to account for entires that have commas in their own name (so the split will split them among multiple
// columns, even though they should be in one) and also for entries that can contain quotes.
// MKM escapes the double quotes by another double quote, so the ending double quote is really ending only if it is not preceded by
// something else.
// So far worst case scenario is a card from the Force of Will game called: "I", the pilot. This in the database looks like this:
// "304732","""I"", the Pilot","1018","Force of Will Single","1775","229401","2017-10-04 17:48:59"
// so it has a comma, double quote precedes it, so it looks like the entry should end there, but it does not, it is in the middle of the name.
// in general, double quote is to be considered opening/ending only if there is an odd number of them, otherwise they are escaped
bool quoteEnclosed = false;
for (int j = 0; j < columnValue.Length; j++)
{
if (columnValue[j] == '"')
quoteEnclosed = !quoteEnclosed;
else break;
}
if (quoteEnclosed) // starts by a double quote -> can contain comma itself, merge until the last double quote is found
{
while (true)
{
quoteEnclosed = false;
for (int j = columnValue.Length - 1; j >= 0; j--) // check if it ends with an odd number of double quotes
{
if (columnValue[j] == '"')
quoteEnclosed = !quoteEnclosed;
else break;
}
if (!quoteEnclosed) // the closing quote was not found yet
columnValue += separator + split[++i]; // append the next column value
else break;
}
}
if (quoteEnclosed)
columnValue = columnValue.Substring(1, columnValue.Length - 2);
columnValue = columnValue.Replace("\"\"", "\""); // un-escape double quotes
// let's handle one corner case: if somebody is exporting a list from excel and says "enclose each field in double quotes",
// empty fields will have the value "". Since that is an even number of double quotes, our algorithm will evaluate it as
// not being quoteEnclosed even though it actually is an enclosed empty string. After the above replacement, we will now
// have a string that is a single double quote. If that is the case, replace it with actual empty string.
// Note that this still does not handle all corner cases, in general, if there is an even number X of double quotes in the field and no other text,
// it can either be X/2 not-enclosed double quotes, or (X-1)/2 enclosed double quotes, there is no way to tell since we are allowing
// mixed format (i.e. enclosed and unenclosed in the same file). Hopefully this never has any practical impact.
if (!quoteEnclosed && columnValue == "\"")
columnValue = "";
ret.Add(columnValue);
}
return ret;
}

/// <summary>
/// Writes the table as CSV.
/// </summary>
/// <param name="filePath">Path to the file as which to write the table.</param>
/// <param name="dt">The data table to write.</param>
public static void WriteTableAsCSV(string filePath, DataTable dt)
{
try
{
using (StreamWriter exp = new StreamWriter(filePath))
{
// we know there will be at least one column, otherwise there would be no valid imported items and therefore no export enabled
string row = "\"" + (dt.Columns[0].ColumnName).Replace("\"", "\"\"") + "\""; // don't forget to escape all " by doubling them
for (int i = 1; i < dt.Columns.Count; i++)
row += ",\"" + (dt.Columns[i].ColumnName).Replace("\"", "\"\"") + "\"";
exp.WriteLine(row);
foreach (DataRow card in dt.Rows)
{
row = "\"" + card[0].ToString().Replace("\"", "\"\"") + "\"";
for (int i = 1; i < dt.Columns.Count; i++)
row += ",\"" + card[i].ToString().Replace("\"", "\"\"") + "\"";
exp.WriteLine(row);
}
}
}
catch (Exception eError)
{
LogError("writing CSV file " + filePath, eError.Message, true);
}
}

/// <summary>
/// Converts the csv to DataTable. <seealso cref="ConvertCSVtoDataTable(StreamReader sr)"/>
/// </summary>
/// <param name="strFilePath">The string file path.</param>
/// <returns>Each row of the file as a row in the returned DataTable.</returns>
public static DataTable ConvertCSVtoDataTable(string strFilePath)
{
using (var sr = new StreamReader(strFilePath))
{
return ConvertCSVtoDataTable(sr);
}
}

/// <summary>
/// Converts the csv to DataTable. <seealso cref="ConvertCSVtoDataTable(StreamReader sr)"/>
/// </summary>
/// <param name="data">Raw data containing the csv file.</param>
/// <returns>Each row of the file as a row in the returned DataTable.</returns>
public static DataTable ConvertCSVtoDataTable(byte[] data)
{
MemoryStream ms = new MemoryStream(data);
using (var sr = new StreamReader(ms))
{
return ConvertCSVtoDataTable(sr);
}
}

/// <summary>
/// Converts a CSV file to a DataTable.
/// http://stackoverflow.com/questions/1050112/how-to-read-a-csv-file-into-a-net-datatable
/// </summary>
/// <param name="sr">Stream with the CSV file. It is assumed that the file has a header on the first line with names of the columns.
/// See parseCSVRow on details on the format of the CSV.</param>
/// <returns>Each row of the file as a row in the returned DataTable.</returns>
/// <exception cref="FormatException">
/// Wrong format of the header of CSV file " + strFilePath + ": " + eError.Message
/// or
/// Wrong format of the CSV file on row " + (dt.Rows.Count + 1) + ": " + eError.Message
/// </exception>
public static DataTable ConvertCSVtoDataTable(StreamReader sr)
{
DataTable dt = new DataTable();
char separator = ',';
try
{
// detect the separator - this assumes it's ether semicolon or comma and that semicolon cannot be part of column names
string firstLine = sr.ReadLine();
if (firstLine.Contains(';'))
separator = ';';
List<string> headers = parseCSVRow(firstLine, separator);
foreach (string header in headers)
dt.Columns.Add(header);
}
catch (Exception eError)
{
throw new FormatException("Wrong format of the header of CSV file: " + eError.Message);
}
while (!sr.EndOfStream)
{
try
{
List<string> row = parseCSVRow(sr.ReadLine(), separator);
DataRow dr = dt.NewRow();
for (int i = 0; i < row.Count; i++)
dr[i] = row[i];
dt.Rows.Add(dr);
}
catch (Exception eError)
{
// technically it is the (dt.Rows.Count + 1)th row, but in the file the first row is the header so this should
// give the user the number of the row in the actual file
throw new FormatException("Wrong format of the CSV file on row " + (dt.Rows.Count + 2) + ": " + eError.Message);
}
}
return dt;
}
}
}
Loading

0 comments on commit 5939d98

Please sign in to comment.