FAQ
Hi all,
I'd like to ask for suggestions regarding suitable datastracture for
storing textual metadata along with a plain text string.
The input format I have is simply text with custom tags like <tag_name
tag_value>; I'd prefer to have this human readable format the original
data source.
For the application, the tags are extracted and stored in a custom
datastructure along with the plain text.The queries should either
return the tagset for a given text position (index) or reversely the
text indices for a given tag-value combination..
(I posted some more detailed remarks earlier, as I was beginning with this topic
http://mail.python.org/pipermail/python-list/2007-December/1130275.html
http://mail.python.org/pipermail/python-list/2008-May/1141958.html

Meanwhile I managed to get working code using python native
datastructures (nested defaultdicts and sets); however after some time
I am now planning to adapt this code for a web-based program and am
considering alternative datastructures, for simplicity I started with
sqlite (which should actually suffice for the data volume and traffic
in question).

I put together some code, which works as expected, but I suspect
somehow, that there must be better ways of doing it.

Two things I am not quite clear about are using the placeholders for
the data identifiers and "chaining" the SELECT parameters.

I Couldn't find a way to use "?" placeholder for table or column
names, hence I ended up using string interpolation for them and
placeholders for the data values, like.
curs.execute('SELECT * FROM "%s" WHERE "%s"==?' % (text_name,
index_col), (text_index,))
is there a better way or is it not supposed to supply these
identifiers programatically?

For getting the matching text indices given the tags, tag_values
combination I ended up with a clumsy query:

combined_query_list = ['SELECT "%s" FROM "%s" WHERE "%s"==?' %
(index_col, text_name, tag) for tag in tags]
sql_query = " INTERSECT ".join(combined_query_list)
curs.execute(sql_query, tag_values)

which produces e.g.:
SELECT "ind" FROM "n" WHERE "KC"==? INTERSECT SELECT "ind" FROM "n"
WHERE "VN"==?

or alternatively:

select_begin = 'SELECT "%s" FROM "%s" WHERE ' % (index_col, text_name)
where_subquery = " AND ".join('"%s"==?' % (tag,) for tag in tags)
sql_query = select_begin + where_subquery

with the resulting query string like:
SELECT "ind" FROM "n" WHERE "KC"==? AND "VN"==? ('12', '1')

(BTW, are these queries equivalent, as the outputs suggest, or are
there some distinctions to be aware of?)

Anyway, I can't really believe, this would be the expected way ...

(I only marginally looked into sqlalchemy, which might simplify this a
bit, is this true? - Currently I only use the standard lib, depending
on the available server setup (python 2.6)).

Thanks in advance for any suggestions or pointers on both the sql
usage as well as the general datatype question.

regards,
Vlastimil Brom

Search Discussions

  • MRAB at Sep 16, 2010 at 11:17 pm

    On 16/09/2010 23:11, Vlastimil Brom wrote:
    Hi all,
    I'd like to ask for suggestions regarding suitable datastracture for
    storing textual metadata along with a plain text string.
    The input format I have is simply text with custom tags like<tag_name
    tag_value>; I'd prefer to have this human readable format the original
    data source.
    For the application, the tags are extracted and stored in a custom
    datastructure along with the plain text.The queries should either
    return the tagset for a given text position (index) or reversely the
    text indices for a given tag-value combination..
    (I posted some more detailed remarks earlier, as I was beginning with this topic
    http://mail.python.org/pipermail/python-list/2007-December/1130275.html
    http://mail.python.org/pipermail/python-list/2008-May/1141958.html

    Meanwhile I managed to get working code using python native
    datastructures (nested defaultdicts and sets); however after some time
    I am now planning to adapt this code for a web-based program and am
    considering alternative datastructures, for simplicity I started with
    sqlite (which should actually suffice for the data volume and traffic
    in question).

    I put together some code, which works as expected, but I suspect
    somehow, that there must be better ways of doing it.

    Two things I am not quite clear about are using the placeholders for
    the data identifiers and "chaining" the SELECT parameters.

    I Couldn't find a way to use "?" placeholder for table or column
    names, hence I ended up using string interpolation for them and
    placeholders for the data values, like.
    curs.execute('SELECT * FROM "%s" WHERE "%s"==?' % (text_name,
    index_col), (text_index,))
    is there a better way or is it not supposed to supply these
    identifiers programatically?
    You would normally expect the structure of the database to be fixed and
    only the contents to vary.
    For getting the matching text indices given the tags, tag_values
    combination I ended up with a clumsy query:

    combined_query_list = ['SELECT "%s" FROM "%s" WHERE "%s"==?' %
    (index_col, text_name, tag) for tag in tags]
    sql_query = " INTERSECT ".join(combined_query_list)
    curs.execute(sql_query, tag_values)

    which produces e.g.:
    SELECT "ind" FROM "n" WHERE "KC"==? INTERSECT SELECT "ind" FROM "n"
    WHERE "VN"==?

    or alternatively:

    select_begin = 'SELECT "%s" FROM "%s" WHERE ' % (index_col, text_name)
    where_subquery = " AND ".join('"%s"==?' % (tag,) for tag in tags)
    sql_query = select_begin + where_subquery

    with the resulting query string like:
    SELECT "ind" FROM "n" WHERE "KC"==? AND "VN"==? ('12', '1')

    (BTW, are these queries equivalent, as the outputs suggest, or are
    there some distinctions to be aware of?)

    Anyway, I can't really believe, this would be the expected way ...
    If you're selecting rows of a table then using 'AND' would seem the
    obvious way.
    (I only marginally looked into sqlalchemy, which might simplify this a
    bit, is this true? - Currently I only use the standard lib, depending
    on the available server setup (python 2.6)).

    Thanks in advance for any suggestions or pointers on both the sql
    usage as well as the general datatype question.

    regards,
    Vlastimil Brom
  • Vlastimil Brom at Sep 16, 2010 at 11:56 pm

    2010/9/17 MRAB <python at mrabarnett.plus.com>:
    On 16/09/2010 23:11, Vlastimil Brom wrote:

    ...
    I put together some code, which works as expected, but I suspect
    somehow, that there must be better ways of doing it.

    Two things I am not quite clear about are using the placeholders for
    the data identifiers and "chaining" the SELECT parameters.

    I Couldn't find a way to use "?" placeholder for table or column
    names, hence I ended up using string interpolation for them and
    placeholders for the data values, like.
    curs.execute('SELECT * FROM "%s" WHERE "%s"==?' % (text_name,
    index_col), (text_index,))
    is there a better way or is it not supposed to supply these
    identifiers programatically?
    You would normally expect the structure of the database to be fixed and
    only the contents to vary.
    For getting the matching text indices given the tags, tag_values
    combination I ended up with a clumsy query:

    combined_query_list = ['SELECT "%s" FROM "%s" WHERE "%s"==?' %
    (index_col, text_name, tag) for tag in tags]
    sql_query = " INTERSECT ".join(combined_query_list)
    curs.execute(sql_query, tag_values)

    which produces e.g.:
    SELECT "ind" FROM "n" WHERE "KC"==? INTERSECT SELECT "ind" FROM "n"
    WHERE "VN"==?

    or alternatively:

    select_begin = 'SELECT "%s" FROM "%s" WHERE ' % (index_col, text_name)
    where_subquery = " AND ".join('"%s"==?' % (tag,) for tag in tags)
    sql_query = select_begin + where_subquery

    with the resulting query string like:
    SELECT "ind" FROM "n" WHERE "KC"==? AND "VN"==? ('12', '1')

    (BTW, are these queries equivalent, as the outputs suggest, or are
    there some distinctions to be aware of?)

    Anyway, I can't really believe, this would be the expected way ...
    If you're selecting rows of a table then using 'AND' would seem the
    obvious way.
    Thanks for the answer,
    Well, that may be a part of the problem, the database structure is
    going to be fixed once I'll have the text sources complete, but I was
    trying to keep it more general, also allowing the identifiers to be
    passed programmatically (based on the tagged text in question).

    yes, I am just selecting rows - based on the combination of the column
    values (which, I guess, might be an usual database approach(?).
    However, I was unsure, whether it is usual to construct the query
    string this way - partly using string interpolation or sequence
    joining.
    Or should there normally be no need for construct like the above and I
    am doing something wrong in a more general sense?

    Thanks again,

    Vlastimil Brom
  • MRAB at Sep 17, 2010 at 12:18 am

    On 17/09/2010 00:56, Vlastimil Brom wrote:
    2010/9/17 MRAB<python at mrabarnett.plus.com>:
    On 16/09/2010 23:11, Vlastimil Brom wrote:

    ...
    I put together some code, which works as expected, but I suspect
    somehow, that there must be better ways of doing it.

    Two things I am not quite clear about are using the placeholders for
    the data identifiers and "chaining" the SELECT parameters.

    I Couldn't find a way to use "?" placeholder for table or column
    names, hence I ended up using string interpolation for them and
    placeholders for the data values, like.
    curs.execute('SELECT * FROM "%s" WHERE "%s"==?' % (text_name,
    index_col), (text_index,))
    is there a better way or is it not supposed to supply these
    identifiers programatically?
    You would normally expect the structure of the database to be fixed and
    only the contents to vary.
    For getting the matching text indices given the tags, tag_values
    combination I ended up with a clumsy query:

    combined_query_list = ['SELECT "%s" FROM "%s" WHERE "%s"==?' %
    (index_col, text_name, tag) for tag in tags]
    sql_query = " INTERSECT ".join(combined_query_list)
    curs.execute(sql_query, tag_values)

    which produces e.g.:
    SELECT "ind" FROM "n" WHERE "KC"==? INTERSECT SELECT "ind" FROM "n"
    WHERE "VN"==?

    or alternatively:

    select_begin = 'SELECT "%s" FROM "%s" WHERE ' % (index_col, text_name)
    where_subquery = " AND ".join('"%s"==?' % (tag,) for tag in tags)
    sql_query = select_begin + where_subquery

    with the resulting query string like:
    SELECT "ind" FROM "n" WHERE "KC"==? AND "VN"==? ('12', '1')

    (BTW, are these queries equivalent, as the outputs suggest, or are
    there some distinctions to be aware of?)

    Anyway, I can't really believe, this would be the expected way ...
    If you're selecting rows of a table then using 'AND' would seem the
    obvious way.
    Thanks for the answer,
    Well, that may be a part of the problem, the database structure is
    going to be fixed once I'll have the text sources complete, but I was
    trying to keep it more general, also allowing the identifiers to be
    passed programmatically (based on the tagged text in question).

    yes, I am just selecting rows - based on the combination of the column
    values (which, I guess, might be an usual database approach(?).
    However, I was unsure, whether it is usual to construct the query
    string this way - partly using string interpolation or sequence
    joining.
    Or should there normally be no need for construct like the above and I
    am doing something wrong in a more general sense?
    In general you'd have a fixed database structure and fixed queries.
    You'd design it so that you wouldn't have different columns for
    different tagged texts, forcing you to change the database structure
    when texts were added or removed, but no-one will come around to your
    house to stop you creating queries dynamically. :-)
  • Vlastimil Brom at Sep 17, 2010 at 8:44 am

    2010/9/17 MRAB <python at mrabarnett.plus.com>:
    On 17/09/2010 00:56, Vlastimil Brom wrote:

    2010/9/17 MRAB<python at mrabarnett.plus.com>:
    On 16/09/2010 23:11, Vlastimil Brom wrote:

    ...
    I put together some code, which works as expected, but I suspect
    somehow, that there must be better ways of doing it.

    Two things I am not quite clear about are using the placeholders for
    the data identifiers and "chaining" the SELECT parameters.

    I Couldn't find a way to use "?" placeholder for table or column
    names, hence I ended up using string interpolation for them and
    placeholders for the data values, like.
    curs.execute('SELECT * FROM "%s" WHERE "%s"==?' % (text_name,
    index_col), (text_index,))
    is there a better way or is it not supposed to supply these
    identifiers programatically?
    You would normally expect the structure of the database to be fixed and
    only the contents to vary.
    For getting the matching text indices given the tags, tag_values
    combination I ended up with a clumsy query:

    combined_query_list = ['SELECT "%s" FROM "%s" WHERE "%s"==?' %
    (index_col, text_name, tag) for tag in tags]
    sql_query = " INTERSECT ".join(combined_query_list)
    curs.execute(sql_query, tag_values)

    which produces e.g.:
    SELECT "ind" FROM "n" WHERE "KC"==? INTERSECT SELECT "ind" FROM "n"
    WHERE "VN"==?

    or alternatively:

    select_begin = 'SELECT "%s" FROM "%s" WHERE ' % (index_col, text_name)
    where_subquery = " AND ".join('"%s"==?' % (tag,) for tag in tags)
    sql_query = select_begin + where_subquery

    with the resulting query string like:
    SELECT "ind" FROM "n" WHERE "KC"==? AND "VN"==? ('12', '1')

    (BTW, are these queries equivalent, as the outputs suggest, or are
    there some distinctions to be aware of?)

    Anyway, I can't really believe, this would be the expected way ...
    If you're selecting rows of a table then using 'AND' would seem the
    obvious way.
    Thanks for the answer,
    Well, that may be a part of the problem, the database structure is
    going to be fixed once I'll have the text sources complete, but I was
    trying to keep it more general, also allowing the identifiers to be
    passed programmatically (based on the tagged text in question).

    yes, I am just selecting rows - based on the combination of the column
    values (which, I guess, might be an usual database approach(?).
    However, I was unsure, whether it is usual to construct the query
    string this way - partly using string interpolation or sequence
    joining.
    Or should there normally be no need for construct like the above and I
    am doing something wrong in a more general sense?
    In general you'd have a fixed database structure and fixed queries.
    You'd design it so that you wouldn't have different columns for
    different tagged texts, forcing you to change the database structure
    when texts were added or removed, but no-one will come around to your
    house to stop you creating queries dynamically. :-)
    --
    http://mail.python.org/mailman/listinfo/python-list
    Ok, thanks for confirming my suspicion :-),
    Now I have to decide whether I shall use my custom data structure,
    where I am on my own, or whether using an sql database in such a
    non-standard way has some advantages...
    The main problem indeed seems to be the fact, that I consider the
    tagged texts to be the primary storage format, whereas the database is
    only means for accessing it more conveniently.

    Thanks again,
    vbr
  • Vlastimil Brom at Sep 18, 2010 at 8:48 am

    2010/9/18 Dennis Lee Bieber <wlfraed at ix.netcom.com>:
    On Fri, 17 Sep 2010 10:44:43 +0200, Vlastimil Brom
    <vlastimil.brom at gmail.com> declaimed the following in
    gmane.comp.python.general:

    Ok, thanks for confirming my suspicion :-),
    Now I have to decide whether I shall use my custom data structure,
    where I am on my own, or whether using an sql database in such a
    non-standard way has some advantages...
    The main problem indeed seems to be the fact, that I consider the
    tagged texts to be the primary storage format, whereas the database is
    only means for accessing it more conveniently.
    ? ? ? ?I suspect part of your difficulty is in trying to fit everything
    into a single relation (table).

    ? ? ? ?Looking back at your ancient "format for storing textual data (for
    an edition) - formatting and additional info" post, I'd probably move
    your so-called tags into one relation -- where the tag type is, itself,
    data...

    ? ? ? ?Without seeing an actual data sample (and pseudo-DDL):

    create table texts
    ? ? ? ?(
    ? ? ? ? ? ? ? ?ID autoincrement primary key,
    ? ? ? ? ? ? ? ?text varchar
    ? ? ? ?);

    create table tags
    ? ? ? ?(
    ? ? ? ? ? ? ? ?ID autoincrement primary key,
    ? ? ? ? ? ? ? ?textID integer foreign key references texts(ID),
    ? ? ? ? ? ? ? ?tagtype char,
    ? ? ? ? ? ? ? ?start integer,
    ? ? ? ? ? ? ? ?end integer,
    ? ? ? ? ? ? ? ?supplement varchar
    ? ? ? ?);

    ? ? ? ?I'd really have to see samples (more than one line) of the raw
    input, and the desired information...

    --
    ? ? ? ?Wulfraed ? ? ? ? ? ? ? ? Dennis Lee Bieber ? ? ? ? AF6VN
    ? ? ? ?wlfraed at ix.netcom.com ? ?HTTP://wlfraed.home.netcom.com/

    --
    http://mail.python.org/mailman/listinfo/python-list
    Thanks for the elaboration, I am sure, I am missing some more advanced
    features of SQL (would the above also work in sqlite, as there
    (probably?) are no real type restrictions on data?
    The "markup" format as well as the requirements haven't change since
    those old posts, one sample of the tagged text is in one of the
    follow-up post of that:

    http://mail.python.org/pipermail/python-list/2008-May/540773.html

    in principle in the tags are in the form <tag_name some tag value>,
    from that text index on this tag-value combination is assigned - until
    <tag_name another value> or </tag_name> arbitrary combinations of the
    tags including overlapping are possible (nesting of the same tags is
    not possible in favor of the direct replacement).
    Different texts may have (partly) differing tags, which I'd prefer to
    handle generally, without having to adapt the queries directly.

    After the tagged text is parsed, the plain text and the corresponding
    "database" are created, which maps the text indices to the tag names
    with their values.
    Querying the data should be able to get the "tagset" for the given
    text index and conversely to find the indices matching the given
    tag-value combinations.

    (actually the text ranges matching those criteria would be even
    better, but these are easily done with bisect)
    (from the specification, mxTextTools looks similar, but it seemed
    rather low-level and quite heavyweight for the given task)

    Thanks in advance for any suggestions,
    Vlastimil Brom
  • Vlastimil Brom at Sep 18, 2010 at 9:00 pm

    2010/9/18 Dennis Lee Bieber <wlfraed at ix.netcom.com>:
    On Sat, 18 Sep 2010 10:48:44 +0200, Vlastimil Brom
    <vlastimil.brom at gmail.com> declaimed the following in
    gmane.comp.python.general:
    ? ? ? ?Ah, based on that listing you are not worried about embedded tags;
    your tags all come at the start of the line (and I presume are
    terminated by the end of line). I'd thought you needed actual positions
    /in/ the line... You can drop the start/end fields and stuff the tag
    attribute into supplement (on SQLite this becomes even simpler since
    even if you define supplement to be integer, SQLite will happily store a
    text value -- a full type checking RDBM would require either making it a
    text field and storing numeric values as text, or using a pair of fields
    for numeric vs text).

    ? ? ? ?Tricky part may be how you handle the display markup -- you seem to
    have a <b> </b> split over two lines... Is that significant?

    ...
    ? ? ? ?Of course, all the search terms can be parameterized when
    programming...

    cur.execute("""select t.ID, tg.supplement, t.text from texts as t
    ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?inner join tags as tg
    ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?on tg.textID = t.ID
    ? ? ? ? ? ? ? ? ? ? ? ?where t.text like ? and tg.type = ?""",
    ? ? ? ? ? ? ? ? ? ? ? ?("%den%", "VN"))

    results = cur.fetchall()

    should return (a Python list of one tuple, in this case):

    [(1, "rn_1_vers_1", "<b>wi den L...n")]
    --
    ? ? ? ?Wulfraed ? ? ? ? ? ? ? ? Dennis Lee Bieber ? ? ? ? AF6VN
    ? ? ? ?wlfraed at ix.netcom.com ? ?HTTP://wlfraed.home.netcom.com/
    Thank you very much for detailed hints, I see, I should have mention
    the specification with my initial post...
    It is true, that nested tags of the same name aren't supported, but
    tags may appear anywhere in the text and aren't terminated with
    newline. The tag-value association is valid from the tag position
    until the next tag replacing the value or closing tag (like </b>) or
    to the end of the text file. Tags beginning at line beginnings are
    rather frequent, but they can appear anywhere else too.
    I actually only store the metadata in the database - i.e. the
    tag-value combinations for the corresponding text indices of the plain
    text. The database doesn't currently contain the text itself; plain
    text is used for fulltext regexp search, and it should be possible to
    find the relevant tags for the matches.
    I'll have a closer look on joins in sql and maybe redesign the data
    structure - now the tags data are copied for each text position with
    some tag change - in order to simplify queries; with multiple tables
    it could be more efficient to store the tags separately and look it up
    individually (probably using bisect (unless there is an SQL equivalent
    ?)
    Well, I still have many areas to investigate in this context ...

    regards,
    Vlastimil Brom
  • Vlastimil Brom at Sep 23, 2010 at 12:28 am

    2010/9/19 Dennis Lee Bieber <wlfraed at ix.netcom.com>:
    On Sat, 18 Sep 2010 23:00:25 +0200, Vlastimil Brom
    <vlastimil.brom at gmail.com> declaimed the following in
    gmane.comp.python.general:
    Thank you very much for detailed hints, I see, I should have mention
    the specification with my initial post...
    It is true, that nested tags of the same name aren't supported, but
    tags may appear anywhere in the text and aren't terminated with
    newline. The tag-value association is valid from the tag position
    until the next tag replacing the value or closing tag (like </b>) or
    ? ? ? ?Okay... Then put the start/end fields back into the database <G>

    ? ? ? ?Also, if the data can span lines, using a foreign key to a table
    having the lines is meaningless... I'd probably generate a plain text
    file (one that does not contain any tags), and record the start/end
    positions for the tags based as a pure character count from start of
    file. Optional if you strip new-lines from this file so it is just a
    long line of text...

    ? ? ? ?Parsing may need to be recursive so you can determine the end points
    for outer tags and generate the full record (type, start, end,
    supplement) {I didn't list ID, though my standard practice is to always
    have an auto-increment ID field in a table -- it simplifies later
    updates}.

    ? ? ? ?Do the tags form a hierarchy? That is, does a "higher level" tag
    force a closure of all unclosed lower levels?
    I'll have a closer look on joins in sql and maybe redesign the data
    structure - now the tags data are copied for each text position with
    some tag change - in order to simplify queries; with multiple tables
    it could be more efficient to store the tags separately and look it up
    individually (probably using bisect (unless there is an SQL equivalent
    ?)
    ? ? ? ?I'm not sure of how you mean "bisect" but I do suggest that you look
    up a lesson on "database normalization" (emphasis on first, second, and
    third normal forms; the others are rather esoteric).

    ? ? ? ?What I'm pretty sure you do NOT want to do is create a table for
    each tag TYPE. The type is just another data value, as in my sample
    tables. That makes queries much simpler since you don't run into the
    problem of having to generate queries where you change the schema
    entities ("schema" is the technical term for the layout of the database
    -- the fixed items rather than the data; end users should never directly
    enter schema items in an application [you can present a menu of schema
    items and let the user pick one, but the selection is never used as-is,
    it has to be restricted to a list you generate, and returns values from
    your list; look up "sql injection"])

    ? ? ? ?With the actual text in a plain file, the joins I was using are not
    needed -- you'd use the start/end results to seek/read the file as a
    separate step.

    ? ? ? ?However, you might have some complex queries that use subselects if
    you need to retrieve a passage inside a passage (poor phrasing).

    ? ? ? ?That is, a subselect that specifies you want the start/end range of
    a tag with a particular attribute, and you use the start/end as >, <
    comparisons to find a different tag. Off the top of my head (hence may
    not be valid SQL)

    select t1.type, t1.supplement, t1.start, t1.end from tags as t1
    ? ? ? ?inner join
    ? ? ? ? ? ? ? ?(select start, end from tags
    ? ? ? ? ? ? ? ? ? ? ? ?where type = "some type"
    ? ? ? ? ? ? ? ? ? ? ? ?and supplement = "some value") as t2
    ? ? ? ?where t1.start >= t2.start and t1.end <= t2.end

    --
    ? ? ? ?Wulfraed ? ? ? ? ? ? ? ? Dennis Lee Bieber ? ? ? ? AF6VN
    ? ? ? ?wlfraed at ix.netcom.com ? ?HTTP://wlfraed.home.netcom.com/

    --
    http://mail.python.org/mailman/listinfo/python-list
    Thanks all for the pointers and recommendations!
    After some tests I eventually found, that I am (for now) probably
    better off with nested (default)dict and set data;
    while querying tags for a given index was very elegant even with a
    very simple database design, I wasn't able to get the other query
    (text interval(s) for given combination of tag-values) with a
    comparably straightforward solution. It seems, that in order to handle
    tag combinations of arbitrary length either programmatically created
    queries or maybe recursion or some kind of postprocessing database
    data using interval arithmetic would be needed (or probably some
    database features, I am not getting :)
    In any case the initial custom datastructure seems to meet the
    requirements after all.

    Thanks again,
    Vlastimil Brom

Related Discussions

Discussion Navigation
viewthread | post
Discussion Overview
grouppython-list @
categoriespython
postedSep 16, '10 at 10:11p
activeSep 23, '10 at 12:28a
posts8
users2
websitepython.org

2 users in discussion

Vlastimil Brom: 6 posts MRAB: 2 posts

People

Translate

site design / logo © 2022 Grokbase