From 6dcd1727eb9d5dbfcb8f6e9599ada10d33063bac Mon Sep 17 00:00:00 2001 From: Simeon Simeonov Date: Wed, 18 Jan 2023 21:12:31 +0100 Subject: Remove some old presentations --- notebooks/python/python_intro.ipynb | 2 +- notebooks/python/python_oo.ipynb | 107 ++++-- reveal.js/dataaccess.html | 360 ------------------- reveal.js/dataporten.html | 118 ------- reveal.js/demo.html | 4 +- reveal.js/evalg.html | 316 ++++++++--------- reveal.js/flow.html | 194 ---------- reveal.js/gateway.html | 152 -------- reveal.js/mqprod.html | 484 ++++++++++++------------- reveal.js/postgresql_tuning.html | 265 +++++++------- reveal.js/python-oo.html | 145 -------- reveal.js/python.html | 680 ------------------------------------ reveal.js/rabbitmq.html | 528 ++++++++++++++-------------- reveal.js/sqlalchemy.html | 301 ++++++++-------- reveal.js/timetravel.html | 246 ++++++------- 15 files changed, 1151 insertions(+), 2751 deletions(-) delete mode 100755 reveal.js/dataaccess.html delete mode 100644 reveal.js/dataporten.html delete mode 100755 reveal.js/flow.html delete mode 100644 reveal.js/gateway.html delete mode 100644 reveal.js/python-oo.html delete mode 100644 reveal.js/python.html mode change 100755 => 100644 reveal.js/sqlalchemy.html diff --git a/notebooks/python/python_intro.ipynb b/notebooks/python/python_intro.ipynb index 3c49a14..0168839 100644 --- a/notebooks/python/python_intro.ipynb +++ b/notebooks/python/python_intro.ipynb @@ -1253,7 +1253,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.8" + "version": "3.11.1" } }, "nbformat": 4, diff --git a/notebooks/python/python_oo.ipynb b/notebooks/python/python_oo.ipynb index b56aa26..1c655d0 100644 --- a/notebooks/python/python_oo.ipynb +++ b/notebooks/python/python_oo.ipynb @@ -179,7 +179,8 @@ "\"The Zen of Python\" (import this) is still relevant and should be followed :)\n", "\n", "\"Python will remain a dynamically typed language, and the authors have no desire to ever make type hints mandatory, even by convention.\" - PEP484\n", - "\n" + "\n", + "Class (type) names should normally use the *CapWords* convention - CapitalizedWords (or *CapWords*, or *CamelCase*). This is also sometimes known as *StudlyCaps*. ASCII characters and English names only!" ] }, { @@ -225,17 +226,17 @@ "name": "stdout", "output_type": "stream", "text": [ - "car1.get_obj_info_str() = \"I am <__main__.Car object at 0x7f5630542810> with id 140008154736656 from with id 94904583728688\"\n", - "car2.get_obj_info_str() = \"I am <__main__.Car object at 0x7f5633ec8190> with id 140008215052688 from with id 94904583728688\"\n", - "car1.model = 'BMW', car1.reg_nr = 'EC76183', car1.extras = ['GPSnav', 'Sound system'], id(car1.cls_extras) = 140008154734272, id(car1.__init__) = 140008154735680\n", - "car2.model = 'Scoda', car2.reg_nr = 'BD77655', car2.extras = ['GPSnav'], id(car2.cls_extras) = 140008154734272, id(car2.__init__) = 140008154736768\n", - "id(Car.cls_extras) = 140008154734272, id(Car.__init__) = 140008154484864\n", - "car1.cls_extras = ['GPSnav', 'Sound system'], id(car1.cls_extras) = 140008154734272\n", - "car2.cls_extras = ['GPSnav', 'Sound system'], id(car2.cls_extras) = 140008154734272\n", + "car1.get_obj_info_str() = \"I am <__main__.Car object at 0x7fc7add96f10> with id 140495591927568 from with id 94628241379728\"\n", + "car2.get_obj_info_str() = \"I am <__main__.Car object at 0x7fc7aef459d0> with id 140495610468816 from with id 94628241379728\"\n", + "car1.model = 'BMW', car1.reg_nr = 'EC76183', car1.extras = ['GPSnav', 'Sound system'], id(car1.cls_extras) = 140495591909184, id(car1.get_obj_info_str) = 140495591927808\n", + "car2.model = 'Scoda', car2.reg_nr = 'BD77655', car2.extras = ['GPSnav'], id(car2.cls_extras) = 140495591909184, id(car2.get_obj_info_str) = 140495591927936\n", + "id(Car.cls_extras) = 140495591909184, id(Car.get_obj_info_str) = 140495591682944\n", + "car1.cls_extras = ['GPSnav', 'Sound system'], id(car1.cls_extras) = 140495591909184\n", + "car2.cls_extras = ['GPSnav', 'Sound system'], id(car2.cls_extras) = 140495591909184\n", "True\n", "hasattr(car1, 'import_tax_paid') = True\n", "hasattr(car2, 'import_tax_paid') = False\n", - "id(car1.__class__) = 94904583728688, id(car2.__class__) = 94904583728688, id(Car) = 94904583728688\n" + "id(car1.__class__) = 94628241379728, id(car2.__class__) = 94628241379728, id(Car) = 94628241379728\n" ] } ], @@ -244,6 +245,7 @@ "\n", " cls_extras = [] # class attribute - shared by all instances\n", " # cls_extras: list = [] is also possible\n", + " car_count = 0\n", "\n", " def __init__(self, model: str, reg_nr: str, extras: list):\n", " \"\"\"\n", @@ -262,24 +264,40 @@ "\n", " # self.cls_extras = extras # will create a *NEW instance attribute* called 'cls_extras'\n", " \n", + " Car.car_count += 1 # increase Car.car_count by 1 every time a car is created \n", + " \n", + " def __del__(self):\n", + " \"\"\"\n", + " Called when the instance is about to be destroyed.\n", + "\n", + " This is also called a finalizer or (improperly) a destructor.\n", + " \"\"\"\n", + " Car.car_count -= 1 # decrease by 1 every time a Car object is destroyed\n", + "\n", " def get_obj_info_str(self): # regular method that becomes instance attribute\n", " return f\"I am {self} with id {id(self)} from {self.__class__} with id {id(self.__class__)}\"\n", "\n", "car1 = Car(\"BMW\", \"EC76183\", [\"GPSnav\", \"Sound system\"])\n", "car2 = Car(\"Scoda\", \"BD77655\", [\"GPSnav\"])\n", "\n", - "print(f\"{car1.get_obj_info_str() = }\")\n", + "print(f\"{car1.get_obj_info_str() = }\") # same as calling Car.get_object_info_str(car1)\n", "print(f\"{car2.get_obj_info_str() = }\")\n", "\n", "# default behavior for `object.attr`:\n", "# getter:\n", "# - checks if 'attr' is an instance attribute\n", - "# - checks if 'attr' is a class attribute\n", + "# - checks if 'attr' is a class attribute (through the method resolution order - MRO)\n", "# - raises AttributeError\n", - "# setter: (re)defines an instance attribute\n", - "print(f\"{car1.model = }, {car1.reg_nr = }, {car1.extras = }, {id(car1.cls_extras) = }, {id(car1.__init__) = }\")\n", - "print(f\"{car2.model = }, {car2.reg_nr = }, {car2.extras = }, {id(car2.cls_extras) = }, {id(car2.__init__) = }\")\n", - "print(f\"{id(Car.cls_extras) = }, {id(Car.__init__) = }\")\n", + "# setter:\n", + "# - (re)defines an instance attribute\n", + "\n", + "# Details not covered in this course:\n", + "# Car.x is translated to Car.__dict__[\"x\"] (...through the MRO)\n", + "# car1.x is translated to car1.__dict__[\"x\"] (if not found ... Car.x (see above), if not found car1.__getattr__(\"x\") is called)\n", + "\n", + "print(f\"{car1.model = }, {car1.reg_nr = }, {car1.extras = }, {id(car1.cls_extras) = }, {id(car1.get_obj_info_str) = }\")\n", + "print(f\"{car2.model = }, {car2.reg_nr = }, {car2.extras = }, {id(car2.cls_extras) = }, {id(car2.get_obj_info_str) = }\")\n", + "print(f\"{id(Car.cls_extras) = }, {id(Car.get_obj_info_str) = }\")\n", "car1.cls_extras.extend(car1.extras)\n", "# car1.cls_extras = car1.extras # N.B. This will create a *NEW instance attribute* called 'cls_extras'\n", "print(f\"{car1.cls_extras = }, {id(car1.cls_extras) = }\")\n", @@ -289,7 +307,7 @@ "car1.import_tax_paid = True # 'import_tax_paid' will only be present in car1\n", "print(car1.import_tax_paid)\n", "print(f\"{hasattr(car1, 'import_tax_paid') = }\") # Out: True\n", - "print(f\"{hasattr(car2, 'import_tax_paid') = }\") # Out: False\n", + "print(f\"{hasattr(car2, 'import_tax_paid') = }\") # Out: False - no 'import_tax_paid' attribute in object car2\n", "\n", "# .__class__ instance attribute pointing to its class (type) will be created\n", "print(f\"{id(car1.__class__) = }, {id(car2.__class__) = }, {id(Car) = }\")" @@ -705,6 +723,7 @@ "name": "stdout", "output_type": "stream", "text": [ + "2:2\n", "__mul__ called\n", "vector * 3 = Vector(start=Point(x=2, y=2), end=Point(x=12, y=18))\n", "__rmul__ called\n", @@ -771,6 +790,7 @@ " # __rmul__ = __mul__\n", "\n", "vector = Vector(Point(2, 2), Point(4, 6))\n", + "print(f\"{vector['start']}\")\n", "print(f\"{vector * 3 = }\")\n", "print(f\"{3 * vector = }\")" ] @@ -863,7 +883,7 @@ "id": "d1d17f26-de6e-4510-b0f0-3ae02761ae24", "metadata": {}, "source": [ - "# Example 2\n", + "# Example 3\n", "\n", "We will create a small and incomplete Animal class hierarchy" ] @@ -973,7 +993,7 @@ " super().__init__(\n", " weight=weight, tooth_replacement=True, alive=alive, **kwargs\n", " )\n", - " self_conservation_status = conservation_status\n", + " self._conservation_status = conservation_status\n", "\n", "\n", "class Cow(Mammal):\n", @@ -1104,6 +1124,7 @@ " \"\"\"\n", "\n", "\n", + "# not good! The user of JSONMixin should not have to explicitly know about / use DictMixin\n", "class JSONConvertableTigerShark(TigerShark, DictMixin, JSONMixin):\n", " \"\"\"A tiger shark class that provides JSON conversion functionality\"\"\"\n", "\n", @@ -1143,14 +1164,7 @@ "MRO is monotonic when the following is true: if C1 precedes C2 in the linearization of C, then C1 precedes C2 in the linearization of any subclass of C.\n", "\n", "Not all classes admit a linearization. There are cases, in complicated hierarchies, where it is not possible to derive a class such that its linearization respects all the desired properties. \n", - "`TypeError: Cannot create a consistent method resolution order (MRO) for bases X, Y` is raised then.\n", - "\n", - "\n", - "## The Liskov substitution principle (LSP):\n", - "\n", - "Functions that use references to base class objects must be able to use objects of derived classes without knowing it.\n", - "\n", - "In other words: an object (such as a class) may be replaced by a sub-object (such as a class that extends the first class) without breaking the program.\n" + "`TypeError: Cannot create a consistent method resolution order (MRO) for bases X, Y` is raised then." ] }, { @@ -1232,6 +1246,14 @@ "help(simeon)" ] }, + { + "cell_type": "markdown", + "id": "875e7ffd-4feb-4ef0-b912-0a939c6c2dd1", + "metadata": {}, + "source": [ + "... **N.B.** The most important thing to note here is that if `super()` is used in `Grandfather1` for this MRO (f.i. object of type `Simeon`) , none of its parents or siblings is called, but `Father` that was probably \"not even born\" when `Grandfather1` was created. :)" + ] + }, { "cell_type": "code", "execution_count": 12, @@ -1274,7 +1296,6 @@ " \"\"\"Produces JSON content for production plans\"\"\"\n", " \n", " def to_json(self, **kwargs):\n", - " # super().get_data(**kwargs)\n", " self.get_data(**kwargs)\n", " prod_plans = self.extract_production_plans(**kwargs)\n", " return json.dumps(self.__do_some_magic(prod_plans), indent=kwargs.get(\"indent\", 2))\n", @@ -1295,7 +1316,10 @@ "\n", "\n", "cool_ts_proprocessor = CoolTSPreprocessor()\n", - "print(cool_ts_proprocessor.to_json(indent=4))" + "print(cool_ts_proprocessor.to_json(indent=4))\n", + "\n", + "# but we want to test CoolTSPreprocessor without the annoying ConnectionManager.get_data method being used\n", + "# simply (temporary or canditionally) replacing ConnectionManager with MockConnectionManager will go against the O in SOLID" ] }, { @@ -1311,6 +1335,7 @@ "Using mock data\n", "Extracting production plans from data\n", "some magic is happening here...\n", + "{}\n", "(, , , , )\n" ] } @@ -1320,10 +1345,34 @@ " pass\n", "\n", "mocked_but_still_cool_ts_preprocessor = MockedButStillCoolTSPreprocessor()\n", - "mocked_but_still_cool_ts_preprocessor.to_json(indent=4)\n", + "print(mocked_but_still_cool_ts_preprocessor.to_json(indent=4))\n", "print(MockedButStillCoolTSPreprocessor.__mro__)" ] }, + { + "cell_type": "markdown", + "id": "738b9084-27b9-4837-a927-e2087267d483", + "metadata": {}, + "source": [ + "# Quick tips and final words\n", + "\n", + "- Avoid the classical pitfall: \"I am solving simple problems by simple means (the way I am used to) and could probably use OO when solving more complex problems\"! Solving simple problems is the best way to get used to the OO way of thinking / designing code.\n", + "\n", + "- Take some time to analyze and design your solution and discuss it with other team members and more experienced programmers. Do not rely only on QA (merge requests)!\n", + "\n", + "- Learn to think in terms of objects, like in real world objects (f.i. Car, Animal, TimeSeries, ProductionPlan, DataSource...)!\n", + "\n", + "- Learn to think in terms of abstract classes (types): Classes that will only serve as a foundation to their child classes, but will themselves never be directly instanciated!\n", + "\n", + "- Do not create \"thin\" classes / objects that simply containin some relevant attributes, protected by corresponding properties! The entire functionality (business logic) of the object should be encapsulated there.\n", + "\n", + "- Always make sure that you are not violating some of the SOLID principles (and others described above)! Run \"mental tests\" to test your current design!\n", + "\n", + "- Consider composition (\"has-a\" relationship) before considering inhiritance (\"is-a\" relationship)!\n", + "\n", + "- Read and learn from free software projects written in Python (f.i. on github.com)" + ] + }, { "cell_type": "markdown", "id": "540de317-ba77-4f5c-97ba-f1c0bec071fb", diff --git a/reveal.js/dataaccess.html b/reveal.js/dataaccess.html deleted file mode 100755 index 4bafccd..0000000 --- a/reveal.js/dataaccess.html +++ /dev/null @@ -1,360 +0,0 @@ - - - - - SQLAlchemy & Odin Data Access - - - - - - - - - - - - - - - -
- - -
- -
-

SQLAlchemy & Odin Data Access

-

Fifty Data Science

-
-

Simeon Simeonov

-
- -
- -
-

Agenda

-
-
    -
  • SQLAlchemy - Design & overview
  • -
  • SQLAlchemy - A small practical example
  • -
  • Odin Data Access - Overview and examples
  • -
  • Q & A
  • -
-
- -
- -
-

What is SQLAlchemy?

-
-

SQLAlchemy is a Python library created by Mike Bayer to provide a high-level Pythonic interface to RDBMS such as PostgreSQL, SQLite, MySQL, Oracle, DB2.

-

SQLAlchemy includes RDBMS-independent SQL expression language and an object-relational mapper (ORM).

-
- -
-

Why use SQLAlchemy?

-
-
    -
  • portability - the programming interface is independent of the type of RDBMS and connector used

  • -
  • security - no more SQL injections

  • -
  • abstraction - no need to bother with complex JOINs

  • -
  • object-orientation - you work with objects and NOT tables and rows

  • -
  • performance - exploits the likehood of reusing a particular query

  • -
  • flexibility - you can override almost anything

  • -
-
- -
-

Basic architecture

-

SQLAlchemy consists of several components, including the the ORM.

-
    -
  • Engine- manages the connection pool and the RDBMS-independent SQL dialect layer
  • -
  • MetaData - used to collect and organize information about your table layout (schema)
  • -
  • Session - establishes all conversations with the RDBMS and represents a "holding zone" for all the objects which you've loaded or associated with it during its lifespan
  • -
  • SQL expression language - provides an API to execute your queries and updates against your tables, all from Python, and all in a database-independent way (low-level interface)
  • -
  • ORM - provides a convenient way to add database persistence to your Python objects withoutrequiring you to design your objects around the database, or the database around the objects (high-level interface)
  • -
- -
- -
-

Example

-

SQLAlchemy gives us the choice between classical mapping and the newer declarative mapping

-
-            
-              import sqlalchemy
-              sqlalchemy.__version__
-
-              from sqlalchemy import create_engine
-
-              # engine = create_engine("postgresql+psycopg2://user:zipassword@localhost/mydb" , echo=True)
-              # The string form of the URL is dialect+driver://user:password@host/dbname[?key=value..],
-              # where dialect is a database name such as mysql, oracle, postgresql, etc.,
-              # and driver the name of a DBAPI, such as psycopg2, pyodbc, cx_oracle
-              # The echo flag is a shortcut to setting up SQLAlchemy logging,
-              # which is accomplished via Python’s standard logging module.
-              # With it enabled, we’ll see all the generated SQL produced.
-              # engine = create_engine("sqlite:///library.db", echo=True)
-              engine = create_engine("sqlite:///:memory:", echo=True)
-
-              metadata = MetaData()
-
-              from sqlalchemy import Table, Column, Integer, String, MetaData, ForeignKey
-
-              authors_table = Table(
-                  "authors",
-                  metadata,
-                  Column("id", Integer, primary_key=True),
-                  Column("name", String),
-              )  # Column("name", String(50)) is possible
-
-              books_table = Table(
-                  "books",
-                  metadata,
-                  Column("id", Integer, primary_key=True),
-                  Column("title", String),
-                  Column("description", String),
-                  Column("author_id", ForeignKey('authors.id')),
-              )
-
-              metadata.create_all(engine)  # creates the tables
-            
-          
-
- -
-

Example (cont...)

-

Use of SQL expression language

-
-            
-              insert_stmt = authors_table.insert(bind=engine)
-              type(insert_stmt)
-              # Out: <class 'sqlalchemy.sql.expression.Insert'>
-              print(insert_stmt)
-              # Out: INSERT INTO authors (id, name) VALUES (:id,:name)
-
-              compiled_stmt = insert_stmt.compile()
-              print(compiled_stmt.params)
-              # Out: {'id': None, 'name': None}
-
-              insert_stmt.execute(name="Alexandre Dumas")  # insert a single entry
-              insert_stmt.execute([{"name": 'Mr X'},{'name': 'Mr Y'}])  # a list of entries
-
-              metadata.bind = engine  # no need to explicitly bind the engine from now on
-              select_stmt = authors_table.select(authors_table.c.id==2)
-              result = select_stmt.execute()
-              result.fetchall()
-              # Out: [(1, u'Mr X')]
-
-              del_stmt = authors_table.delete()
-              del_stmt.execute(whereclause=text("name='Mr Y'"))
-              del_stmt.execute()  # delete all
-            
-          
-
- -
-

Example (cont...)

-

Use of classical mapping

-
-            
-              from sqlalchemy.orm import mapper
-              from sqlalchemy.orm import relationship, backref
-
-              class Author:
-                  def __init__(self, name):
-                      self.name = name
-
-                  def __str__(self):
-                      return self.name
-
-
-              class Book:
-                  def __init__(self, title, description, author):
-                      self.title = title
-                      self.description = description
-                      self.author = author
-
-                  def __str__(self):
-                      return self.title
-
-              mapper(Book, books_table)
-              mapper(Author, authors_table, properties = {"books": relation(Book, backref="author")})
-            
-          
-
- -
-

Example (cont...)

-

Doing the same thing the easy way with declarative mapping

-
-            
-              from sqlalchemy.ext.declarative import declarative_base
-              from sqlalchemy.orm import relationship, backref
-
-              Base = declarative_base()
-
-              class Author(Base):
-                  __tablename__ = "authors"
-
-                  id = Column(Integer, primary_key=True)
-                  name = Column(String)
-
-                  def __init__(self, name):
-                      self.name = name
-
-                  def __str__(self):
-                      return self.name
-
-
-              class Book(Base):
-                  __tablename__ = "books"  # self.__table__ will be available for our objects
-
-                  id = Column(Integer, primary_key=True)
-                  title = Column(String)
-                  description = Column(String)
-                  author_id = Column(Integer, ForeignKey("authors.id"))
-                  author = relationship(Author, backref=backref("books", order_by=title))
-
-                  def __init__(self, title, description, author):
-                      self.title = title
-                      self.description = description
-                      self.author = author
-
-                  def __str__(self):
-                      return self.title
-
-              Base.metadata.create_all(engine)# create tables
-            
-          
-
- -
-

Example (cont...)

-

Creating instances

-
-            
-              from sqlalchemy.orm import sessionmaker
-
-              Session = sessionmaker(bind=engine)  # bound session
-              session = Session()
-
-              author_1 = Author("Richard Dawkins")
-              author_2 = Author("Matt Ridley")
-
-              book_1 = Book("The Red Queen", "A popular science book", author_2)
-              book_2 = Book("The Selfish Gene", "A popular science book", author_1)
-              book_3 = Book("The Blind Watchmaker", "The theory of evolutio", author_1)  # typo
-
-              session.add(author_1)
-              session.add(author_2)
-              session.add(book_1)
-              session.add(book_2)
-              session.add(book_3)  # or simply session.add_all([author_1, author_2, book_1, book_2, book_3])
-
-              # session.flush()
-              session.commit()  # flushes (issues the statements and sends them to the RDBMS) and commits
-
-              book_3.description = "The theory of evolution"  # update the object
-              book_3 in session   # check whether the object is in the session
-              # Out: True
-
-              session.commit()
-            
-          
-
- -
-

Example (cont...)

-

Queries

-
-            
-              session.query(Book).order_by(Book.id)  # returns a Query instance with a .statement attribute
-              session.query(Book).order_by(Book.id).all()  # returns an object-list
-
-              # return all book objects where title == "The Selfish Gene"
-              session.query(Book).filter(Book.title == "The Selfish Gene").order_by(Book.id).all()
-
-              # using LIKE
-              session.query(Book).filter(Book.title.like("The%")).order_by(Book.id).all()
-
-              query = session.query(Book).filter(Book.id == 9).order_by(Book.id)
-              query.count()  # returns 0L
-              query.all()  # returns an empty list
-              query.first()  # returns None
-              query.one()  # raises NoResultFound exception
-
-              query = session.query(Book).filter(Book.id == 1).order_by(Book.id)
-              book_1 = query.one()
-              book_1.description  # returns "A popular science book"
-              book_1.author.books  # returns a list of Book-objects representing all the books from the same author.
-
-              # get a list of all Book-instances where the author"s name is "Richard Dawkins"
-              session.query(Book).filter(Book.author_id == Author.id).filter(Author.name == "Richard Dawkins").all()
-              session.query(Book).join(Author).filter(Author.name == "Richard Dawkins").all()
-              session.query(Book).\
-                  from_statement("SELECT b.* FROM books b, authors a WHERE b.author_id = a.id AND a.name=:name").\
-                  params(name="Richard Dawkins").all()
-            
-          
-
- -
-

... simply amazing!

-
- -
-

Odin Data Access

-

https://gitlab.fifty.eu/odin/data-science/odin-data-access

-

Provides the odin_data_access module with some of the following entities:

-
    -
  • session module for creating Session objects in a Fifty environment via get_session and get_session_from_env
  • -
  • DBBase - a tiny base-class that can be used as a mixin for mapping
  • -
  • DBTableAccessor - tiny wrapper for Session and Table as well a "facade" for Query and pandas
  • -
-

The DBTableAccessor:

-
    -
  • represents a single RDBMS-table that is introspected during instance initialization (__init__)
  • -
  • uses a mix of ORM and SQL expression language functionality
  • -
  • contins a mapper for mapping custom classes to its corresponding Table
  • -
  • used by Batman and Chuck Norris (source: incomplete Google search)
  • -
-
- -
-

Demo

-

Now watch this well-rehearsed demo...

-
- -
-

Q & A

-
- -
-
- - - - - - - - - - - diff --git a/reveal.js/dataporten.html b/reveal.js/dataporten.html deleted file mode 100644 index 6c9f941..0000000 --- a/reveal.js/dataporten.html +++ /dev/null @@ -1,118 +0,0 @@ - - - - - Utviklerforum: Dataporten & eValg 3.0 - - - - - - - - - - - - - - -
- - -
-
-

Dataporten & eValg 3.0

-

Utviklerforum (31.08.2017)

-
-

- Simeon Simeonov -

-
- -
-

Agenda

-
-
    -
  • Intro

  • -
  • The big picture

  • -
  • Flow

  • -
  • Q & A

  • -
-
- -
-
-

Introduction

-

Evalg 3.0

-
    -
  • Development started in 2017

  • -
  • Dataporten / Feide (OIDC with discovery) ... perhaps others in the future?

  • -
  • Python 3.6 / Flask / React / pyoic / pyJWT ...

  • -
  • Using fnr, feide-id, names...

  • -
-
-
-

Challenges with Dataporten

-
    -
  • React

  • -
  • Flexibility (using other providers (OP))

  • -
  • Testing (users, firewall(s))

  • -
-
-
- -
-

The big picture

- -
- -
-
-

Flow

- -
-
-

Challenges

-
    -
  • Not distributing client secrets

  • -
  • Checking state and verifying signatures

  • -
  • Flexibility (using other providers (OP))

  • -
  • Testing (users, firewall(s))

  • -
-
-
- -
-

Q & A

-

???

-

“Judge a man by his questions rather than by his answers.” - Voltaire

-
- -
-
- - - - - - - - - - - diff --git a/reveal.js/demo.html b/reveal.js/demo.html index ab11f77..39b014d 100644 --- a/reveal.js/demo.html +++ b/reveal.js/demo.html @@ -86,7 +86,7 @@

Pretty Code

-

+					

 						import React, { useState } from 'react';
 
 						function Example() {
@@ -102,7 +102,7 @@
 
 				

With animations

-

-	  
-	  
-
-  
+    
+        
+        Utviklerforum: eValg 3
+        
+        
+        
+        
+
+        
+        
+
+        
+
+        
+        
+    
+    
+        
+ + +
+
+

eValg 3

+

Utviklerforum (28.11.2019)

+
+

Simeon Simeonov (sgs @ USITINT)

+
+ +
+

Agenda

+
+
    +
  • Introduction

  • +
  • Components

  • +
  • Development

  • +
  • Q & A

  • +
+
+ +
+

Introduction

+

eValg 3

+
    +
  • Development started in 2017 and then again in 2018

  • +
  • The last trial election (https://valg-pilot.uio.no) ended 15.11.2019

  • +
  • Currently the eValg team consists of two developers (skh, sgs)

  • +
+
+ + +
+
+

Components

+
+
    +
  • Backend

  • +
  • Dataporten

  • +
  • Frontend

  • +
+
+ +
+

Backend

+
    +
  • General: Python >= 3.6, Flask, SQLAlchemy

  • +
  • DB: Alembic, SQLAlchemy-continuum, Flask-migrate

  • +
  • Signing & encryption: pynacl

  • +
  • Querying (GraphQL): graphene, graphene-sqlalchemy

  • +
  • Others: pytz, sentry_sdk, python-json-logger and many more

  • +
+
+ +
+

Dataporten

+
    +
  • OpenID Connect: Moved from Authorization code flow to Implicit flow (id_token token) in 2018

  • +
+
+ +
+

Frontend

+
    +
  • General: React, react-oidc, GraphQL

  • +
  • Encryption: TweetNaCl - a crypto library in 100 tweets

  • +
  • Many, many more libraries and packages

  • +
+
+ +
+ + +
+ +
+

Development

+
+
    +
  • Improvements

  • +
  • Goals & challenges

  • +
+
+ +
+

Improvements

+
    +
  • Testing (pytest) & CI (Jenkins) - the best in USITINT so far?!? :)

  • +
  • No more FNR* and cross domain access. #GDPR

  • +
  • No more external programs (GnuPG, evalg.jar) and envelopes ...

  • +
  • Insight in all possible election paths in case of drawing

  • + +
+

* - work in progress

+
+ +
+

Goals & challenges

+
    +
  • Anyone should be able to create an election

  • +
  • No private key shall ever live in the backend

  • +
  • Log & audit all changes

  • +
  • It is never too late to change a pollbook

  • +
  • It is never too late to master public-key cryptography ... :)

  • +
+
+ +
+ + +
+

Q & A

+

???

+

“Old questions are not answered—they only go out of fashion” - Donald Schön

+
+
+
+ + + + + + + + + + diff --git a/reveal.js/flow.html b/reveal.js/flow.html deleted file mode 100755 index c5b2ef3..0000000 --- a/reveal.js/flow.html +++ /dev/null @@ -1,194 +0,0 @@ - - - - - QA flow - - - - - - - - - - - - - - -
- - -
-
-

QA flow

-

Idas and thoughts

-
-

- Simeon Simeonov -

-
- -
-
-

Agenda

-
-
    -
  • The problem with the current QA flow
  • -
  • Alternatives
  • -
  • Suggestions
  • -
  • Q&A and discussion
  • -
-
-
- - -
-
-

The problem with the current QA flow

-

“Technical debt is a concept in software development that reflects the implied cost of additional rework caused by choosing an easy (limited) solution now instead of using a better approach that would take longer.” - Wikipedia

-
-

“Accumulation of technical debt is not always a choice.” - Simeon Simeonov, 21st century developer

-
- -
-

The problem with the current QA flow (2)

-
-
    -
  • Based on pull-requests (tactical)
  • -
  • Poor or lack of technical (strategic) design
  • -
  • Lack of supervision
  • -
-
- -
-

The problem with the current QA flow (3)

-
-

Typical challenges

-
    -
  • What entities do we need and how will we name them? (module, package, a script...)
  • -
  • Where will the code reside?
  • -
  • What is the best way to interract? (Object-oriented design in the case of Python)
  • -
  • Is this the best way (even in pragmatic terms)?
  • -
  • Any known best practices?
  • -
-
- -
- - -
-
-

Alternatives

-
-

What do others do?

-
- -
-

Alternatives (2)

-
-

Google, Microsoft...

-
    -
  • Detailed technical design is expensive and requires hierarchial structure
  • -
  • They do not exploit our logistical advantage of being in one place
  • -
-
- -
-

Alternatives (3)

-
-

Linux...

-
    -
  • Brutal and hierarchial structure with a lot of overhead
  • -
  • They do not exploit our logistical advantage of being in one place
  • -
-
- -
- - -
- -
-

Suggestions

-
-
    -
  • Automate most of the current QA tasks through better test coverage, static checking and linting
  • -
  • Modify the QA flow
  • -
  • Consider a more detailed technical specification(s)
  • -
-
- -
-

Automation

-
-
    -
  • better test coverage - as much as possible, something is better than nothing
  • -
  • own flake / pylint modules, as well as disabling the annoying ones as part of CI
  • -
-
- -
-

New QA flow

-
-
    -
  1. Decide whether the task is "strategical" during planning and mark it accordingly in Jira
  2. -
  3. Assign supervising developer once the task is picked
  4. -
  5. Commence the design phase(*)
  6. -
  7. Approval by the supervising developer should be required
  8. -
-
- -
-

New QA flow (2)

-
-

Design phase

-
    -
  1. Identify all entities, entry points and parameters (in case of CLI)
  2. -
  3. Decide on names, naming conventions and where different enteties will reside
  4. -
  5. Decide on code entities and define the public interface (OO design)
  6. -
-
- -
-

A more detailed technical specification?

-
-

May be feasible when developing new systems and micro-services

-
- -
- - -
-

Q&A

-
- - -
-
- - - - - - - - - - - diff --git a/reveal.js/gateway.html b/reveal.js/gateway.html deleted file mode 100644 index 4996a9c..0000000 --- a/reveal.js/gateway.html +++ /dev/null @@ -1,152 +0,0 @@ - - - - - API gateway - - - - - - - - - - - - - - - - - -
- - -
-
-

API gateway

-

Cerebrum-seminar 2016

-
-

- Simeon Simeonov -

-
- -
-

Simeon Simeonov

-
- -
- - -
-

API gateway vs. API proxy

-
-

The integration project at UiO (INT) defines the role API Manager. Why use API gateway instead of API proxy?

-
    -
  • Unified access for all our APIs

  • -
  • Unified documentation and catalogue

  • -
  • Unified caching for all APIs

  • -
  • Unified logging / statistics generation for all APIs

  • -
  • Access control / rate limiting for all our APIs

  • -
  • (Re)mapping and (re)writing

  • -
-
- - -
-
-

API gateway & Cerebrum

-
-

For the time being we have settled on Tyk API gateway.

-

It uses an internal user-database and consists of the following logical components:

-
    -
  • Dashboard

  • -
  • Developer portal

  • -
  • Gateway

  • -
-
- -
-

Dashboard

-

Provides a convenient web-interface for configuration and manipulation of the gateway and the different APIs.

- - -
- -
-

Portal

- Provides the API documentation and catalogue. - -
- -
-

Portal

- -
- -
-

Gataway

-
-
    -
  • Still in experimental / pilot phase

  • -
  • Several APIs are already configured in Tyk (Cerebrum WS, SAP WS... )

  • -
  • Support for many authentication modes (Auth Token, JWT, OAuth 2, Basic Auth, Open ID Connect, ...)

  • -
-

- Official site: https://tyk.io/ -

-
- -
- - -
-

Q&A

-
- - -
-
- - - - - - - diff --git a/reveal.js/mqprod.html b/reveal.js/mqprod.html index fb3caf8..9aef4bc 100644 --- a/reveal.js/mqprod.html +++ b/reveal.js/mqprod.html @@ -1,248 +1,248 @@ - - - Meldingskø for UiO - - - - - - - - - - - - - - -
- - -
-
-

Meldingskø for UiO

-

Utviklerforum 14.12.2017

-
-

- Kai Vaade (KIA), Simeon Simeonov (INT) -

-
- -
-

Agenda

-
-
    -
  • Administrasjon
  • -
  • Roller og entiteter
  • -
  • Flyt
  • -
  • Rettigheter
  • -
  • Q & A
  • -
-
- -
-

Generelt

+ + + Meldingskø for UiO + + + + + + + + + + + + + + +
+ + +
+
+

Meldingskø for UiO

+

Utviklerforum 14.12.2017

+
+

+ Kai Vaade (KIA), Simeon Simeonov (INT) +

+
+ +
+

Agenda

+
+
    +
  • Administrasjon
  • +
  • Roller og entiteter
  • +
  • Flyt
  • +
  • Rettigheter
  • +
  • Q & A
  • +
+
+ +
+

Generelt


-

Vi bruker RabbitMQ med AMQP 0.9.1 til å utveksle / behandle meldinger (JSON, SCIM)

-

Bruk av message-broker startet i 2015. I produksjon siden høsten 2017.

-

Driftes av KIA.

-
- -
-

Administrasjon

-

Cerebrum har brukt MQ et års tid på egen server satt opp av Seksjon for integrasjon og elektroniske identiteter (USITINT).

-

Nå er driften av tjenesten flyttet fra prosjektet og inn i linja, Gruppe for drift av katalog-, integrasjon- og autentiseringstjenester (KIA).

-
- -
-

Administrasjon (forts...)

-

Vi har softlaunchet meldingskø-tjenesten i høst. Tjenesten er ikke "offisielt" lansert, kjører kun våre egne ting (Cerebrum/SAP) foreløpig.

-

Antakelig gjør vi tjenesten kjent samtidig som et annet delprosjekt (API Manager) i UiO INTARK lanseres i nær fremtid.

-
- -
-

Administrasjon (forts...)

-

Foreløpig så har vi ikke så veldig mye erfaring med tjenesten i linja.

-

Vi vet heller ikke så mye om behovet; dvs hvor mange som kommer til å bruke tjenesten.

-
- -
-

Administrasjon (forts...)

-

Hvordan kontakte oss:

-

https://www.usit.uio.no/om/organisasjon/iti/td/kia/dokumentasjon/meldingsko/

-

Tilgang til grensesnittet: https://mq.uio.no

-
- -
-

Roller og entiteter

-
    -

    Roller:

    -
  • Administrator / Manager
  • -
  • Consumer / Konsument
  • -
  • Publisher / Publisist :)
  • -

    Entiteter:

    -
  • vhost
  • -
  • Exchange (direct, fanout, topic, ...)
  • -
  • Kø (transient, durable)
  • -
  • Binding
  • -
-
- -
-

Flyt

+

Vi bruker RabbitMQ med AMQP 0.9.1 til å utveksle / behandle meldinger (JSON, SCIM)

+

Bruk av message-broker startet i 2015. I produksjon siden høsten 2017.

+

Driftes av KIA.

+
+ +
+

Administrasjon

+

Cerebrum har brukt MQ et års tid på egen server satt opp av Seksjon for integrasjon og elektroniske identiteter (USITINT).

+

Nå er driften av tjenesten flyttet fra prosjektet og inn i linja, Gruppe for drift av katalog-, integrasjon- og autentiseringstjenester (KIA).

+
+ +
+

Administrasjon (forts...)

+

Vi har softlaunchet meldingskø-tjenesten i høst. Tjenesten er ikke "offisielt" lansert, kjører kun våre egne ting (Cerebrum/SAP) foreløpig.

+

Antakelig gjør vi tjenesten kjent samtidig som et annet delprosjekt (API Manager) i UiO INTARK lanseres i nær fremtid.

+
+ +
+

Administrasjon (forts...)

+

Foreløpig så har vi ikke så veldig mye erfaring med tjenesten i linja.

+

Vi vet heller ikke så mye om behovet; dvs hvor mange som kommer til å bruke tjenesten.

+
+ +
+

Administrasjon (forts...)

+

Hvordan kontakte oss:

+

https://www.usit.uio.no/om/organisasjon/iti/td/kia/dokumentasjon/meldingsko/

+

Tilgang til grensesnittet: https://mq.uio.no

+
+ +
+

Roller og entiteter

+
    +

    Roller:

    +
  • Administrator / Manager
  • +
  • Consumer / Konsument
  • +
  • Publisher / Publisist :)
  • +

    Entiteter:

    +
  • vhost
  • +
  • Exchange (direct, fanout, topic, ...)
  • +
  • Kø (transient, durable)
  • +
  • Binding
  • +
+
+ +
+

Flyt


-
- -
-

Rettigheter

-

RabbitMQ implementerer 2 rettighetsnivåer: per vhost og per entitet

-

RabbitMQ (AMQP) definerer 3 typer operasjoner:

-
    -
  • configure - opprette / slette entiteter eller endre deres oppførsel

  • -
  • write - skrive melding til en entitet

  • -
  • read - lese melding fra entitet

  • -
-
- -
-

Rettigheter og brukere

-

RabbitMQ bruker regular expressions til å definere rettigheter

-
    -
  • rabbitmqctl add_user cerebrum <passord>

  • -
  • rabbitmqctl add_user uio_ad_microservice <passord>

  • -
  • rabbitmqctl set_permissions -p /no/uio/integration cerebrum "^$" "^ex_.*" "^$"

  • -
  • rabbitmqctl set_permissions -p /no/uio/integration uio_ad_microservice "^q_ad_ms_.*" "^q_ad_ms_.*" "^(ex_messages|q_ad_ms_.*)$"

  • -
-
- -
-

Routing keys i topic exchange

-

Routing key settes av sender som en del av meldingen og blir inspisert av brokeren dersom meldingen sendes til en topic exchange.

-

Strukturen til en topic / message routing key er:

-

<kilde>.<type>.<objekt>.<hendelse>

-

F.eks.:

-

cerebrum.event.person.delete

-
- -
-

Dokumentasjon og lenker

-

- RabbitMQ: -

- -
- -
-

Dokumentasjon og lenker

-

- UiO: -

- -

- Bøker: -

-
    -
  • -

    - - RabbitMQ in action - Alvaro Videla / Jason J.W. Williams - 2012 - Manning - -

  • -
  • -

    - - Mastering RabbitMQ - Ayanoglu / Aytas / Nahum - 2015 - PACKT Publishing - -

  • -
- -
-

Q & A

-
- -
-
- - - - - - - - - - +
+ +
+

Rettigheter

+

RabbitMQ implementerer 2 rettighetsnivåer: per vhost og per entitet

+

RabbitMQ (AMQP) definerer 3 typer operasjoner:

+
    +
  • configure - opprette / slette entiteter eller endre deres oppførsel

  • +
  • write - skrive melding til en entitet

  • +
  • read - lese melding fra entitet

  • +
+
+ +
+

Rettigheter og brukere

+

RabbitMQ bruker regular expressions til å definere rettigheter

+
    +
  • rabbitmqctl add_user cerebrum <passord>

  • +
  • rabbitmqctl add_user uio_ad_microservice <passord>

  • +
  • rabbitmqctl set_permissions -p /no/uio/integration cerebrum "^$" "^ex_.*" "^$"

  • +
  • rabbitmqctl set_permissions -p /no/uio/integration uio_ad_microservice "^q_ad_ms_.*" "^q_ad_ms_.*" "^(ex_messages|q_ad_ms_.*)$"

  • +
+
+ +
+

Routing keys i topic exchange

+

Routing key settes av sender som en del av meldingen og blir inspisert av brokeren dersom meldingen sendes til en topic exchange.

+

Strukturen til en topic / message routing key er:

+

<kilde>.<type>.<objekt>.<hendelse>

+

F.eks.:

+

cerebrum.event.person.delete

+
+ +
+

Dokumentasjon og lenker

+

+ RabbitMQ: +

+ +
+ +
+

Dokumentasjon og lenker

+

+ UiO: +

+ +

+ Bøker: +

+
    +
  • +

    + + RabbitMQ in action - Alvaro Videla / Jason J.W. Williams - 2012 - Manning + +

  • +
  • +

    + + Mastering RabbitMQ - Ayanoglu / Aytas / Nahum - 2015 - PACKT Publishing + +

  • +
+ +
+

Q & A

+
+ +
+
+ + + + + + + + + + diff --git a/reveal.js/postgresql_tuning.html b/reveal.js/postgresql_tuning.html index d7e3466..8678513 100644 --- a/reveal.js/postgresql_tuning.html +++ b/reveal.js/postgresql_tuning.html @@ -1,152 +1,151 @@ - - - SQLAlchemy - - - - - - - - - - - - - - - -
- - -
- -
-

PostgreSQL tuning

-

Data Science @ Beryl

+ + + SQLAlchemy + + + + + + + + + + + + + + + +
+ + +
+ +
+

PostgreSQL tuning

+

Data Science @ Beryl


-

Simeon Simeonov

-
+

Simeon Simeonov

+
-
+
-
-

Agenda

+
+

Agenda


    -
  • Generic tools for gathering information
  • -
  • Memory settings
  • -
  • Logging and performance reports
  • -
  • Other tools for analysis
  • +
  • Generic tools for gathering information
  • +
  • Memory settings
  • +
  • Logging and performance reports
  • +
  • Other tools for analysis
-
+
-
+
-
-

General tools for gathering information

+
+

General tools for gathering information


-            
-              # shell
-              # fetch information from the OS
-              cat /proc/cpuinfo
-              cat /proc/meminfo
-              sysctl -a | grep shm  # get kernel parameters of interest
-            
-            
-              -- SQL
-              -- show the current values of all settings
-              SHOW ALL;
-
-              -- display even more than all...
-              SELECT * FROM pg_settings;
-
-              -- opening postgresql.conf and reading the comments - the old school approach
-            
-            
-              # old school: edit postgresql.conf and read the comments
-            
+              
+                  # shell
+                  # fetch information from the OS
+                  cat /proc/cpuinfo
+                  cat /proc/meminfo
+                  sysctl -a | grep shm  # get kernel parameters of interest
+              
+              
+                  -- SQL
+                  -- show the current values of all settings
+                  SHOW ALL;
+
+                  -- display even more than all...
+                  SELECT * FROM pg_settings;
+
+                  -- opening postgresql.conf and reading the comments - the old school approach
+              
+              
+                  # old school: edit postgresql.conf and read the comments
+              
           
-
+
-
-

Memory settings

+
+

Memory settings


-
    -
  • shared_buffers - how much memory is dedicated to PostgreSQL to use for caching data - for a system with 1GB or more of RAM, a reasonable starting value for shared_buffers is 25% of the system memory (128MB -> 1GB)
  • -
  • effective_cache_size - how much memory we expect to be available in the OS and PostgreSQL buffer caches, not an allocation - used only by the PostgreSQL query planner to figure out whether plans it's considering would be expected to fit in RAM or not - 1/2 of total memory would be a normal conservative setting (4GB -> 8GB)
  • -
  • work_mem - the base maximum amount of memory to be used by a query operation (such as a sort or hash table) before writing to temporary disk files - for a complex query, several sort or hash operations might be running in parallel; each operation will generally be allowed to use as much memory as this value specifies (4MB -> 8MB)
  • -
  • maintenance_work_mem - the maximum amount of memory to be used by maintenance operations, such as VACUUM and CREATE INDEX. It's safe to set this value significantly larger than work_mem (64MB -> 256MB)
  • -
-
- -
-

Logging and performance reports

+
    +
  • shared_buffers - how much memory is dedicated to PostgreSQL to use for caching data - for a system with 1GB or more of RAM, a reasonable starting value for shared_buffers is 25% of the system memory (128MB -> 1GB)
  • +
  • effective_cache_size - how much memory we expect to be available in the OS and PostgreSQL buffer caches, not an allocation - used only by the PostgreSQL query planner to figure out whether plans it's considering would be expected to fit in RAM or not - 1/2 of total memory would be a normal conservative setting (4GB -> 8GB)
  • +
  • work_mem - the base maximum amount of memory to be used by a query operation (such as a sort or hash table) before writing to temporary disk files - for a complex query, several sort or hash operations might be running in parallel; each operation will generally be allowed to use as much memory as this value specifies (4MB -> 8MB)
  • +
  • maintenance_work_mem - the maximum amount of memory to be used by maintenance operations, such as VACUUM and CREATE INDEX. It's safe to set this value significantly larger than work_mem (64MB -> 256MB)
  • +
+
+ +
+

Logging and performance reports


-

pgBadger - a fast PostgreSQL log analysis report can be used for general analysis.

-
    -
  • log_checkpoints - checkpoints and restartpoints are logged in the server log. Some statistics are included in the log messages, including the number of buffers written and the time spent writing them
  • -
  • log_connections - each attempted connection to the server to be logged, as well as successful completion of client authentication
  • -
  • log_disconnections - provides information similar to log_connections, plus the duration of the session
  • -
  • log_line_prefix - set to '%t [%p]: user=%u,db=%d,app=%a,client=%h '
  • -
  • log_lock_waits - log message is produced when a session waits longer than deadlock_timeout to acquire a lock. This is useful in determining if lock waits are causing poor performance
  • -
  • log_temp_files - when set to 0, a log entry is emitted for each temporary file when it is deleted
  • -
  • log_autovacuum_min_duration - set to 0 it logs all autovacuum actions
  • -
-
- -
-

Other tools for analysis

+

pgBadger - a fast PostgreSQL log analysis report can be used for general analysis.

+
    +
  • log_checkpoints - checkpoints and restartpoints are logged in the server log. Some statistics are included in the log messages, including the number of buffers written and the time spent writing them
  • +
  • log_connections - each attempted connection to the server to be logged, as well as successful completion of client authentication
  • +
  • log_disconnections - provides information similar to log_connections, plus the duration of the session
  • +
  • log_line_prefix - set to '%t [%p]: user=%u,db=%d,app=%a,client=%h '
  • +
  • log_lock_waits - log message is produced when a session waits longer than deadlock_timeout to acquire a lock. This is useful in determining if lock waits are causing poor performance
  • +
  • log_temp_files - when set to 0, a log entry is emitted for each temporary file when it is deleted
  • +
  • log_autovacuum_min_duration - set to 0 it logs all autovacuum actions
  • +
+
+ +
+

Other tools for analysis


-
    -
  • ANALYZE - collects statistics about the contents of tables in the database, and stores the results in the pg_statistic system catalog. Subsequently, the query planner uses these statistics to help determine the most efficient execution plans for queries.
  • -
  • VACUUM - reclaims storage occupied by dead tuples. In normal PostgreSQL operation, tuples that are deleted or obsoleted by an update are not physically removed from their table; they remain present until a VACUUM is done. (VACUUM vs. VACUUM FULL)
  • -
-
- -
-

Sources

+
    +
  • ANALYZE - collects statistics about the contents of tables in the database, and stores the results in the pg_statistic system catalog. Subsequently, the query planner uses these statistics to help determine the most efficient execution plans for queries.
  • +
  • VACUUM - reclaims storage occupied by dead tuples. In normal PostgreSQL operation, tuples that are deleted or obsoleted by an update are not physically removed from their table; they remain present until a VACUUM is done. (VACUUM vs. VACUUM FULL)
  • +
+
+ +
+

Sources


-

- https://www.postgresql.org/docs/ - The official documentation -

-

- https://wiki.postgresql.org - The official Wiki -

-
- -
-

Q & A

-
- -
-
- - - - - - - - - - +

+ https://www.postgresql.org/docs/ - The official documentation +

+

+ https://wiki.postgresql.org - The official Wiki +

+
+ +
+

Q & A

+
+ + + + + + + + + + + + + diff --git a/reveal.js/python-oo.html b/reveal.js/python-oo.html deleted file mode 100644 index 5279993..0000000 --- a/reveal.js/python-oo.html +++ /dev/null @@ -1,145 +0,0 @@ - - - - - Object-oriented programming in Python - - - - - - - - - - - - - - - -
- - -
- -
-

Object-oriented programming in Python

-

Data Engineering @ Statnett

-
-

Simeon Simeonov

-
- -
-

Goals

-
-
    -
  • Present the Python programming language in a different way than https://docs.python.org

  • -
  • Avoid information overload

  • -
  • Use examples and interaction rather than documents and slides

  • -
-
- -
-

Preliminary plan

-
-
    -
  • Basics: About the language, the Python eco-system, types, modules, functions, scopes, decorators, string formatting

  • -
  • Object-oriented programming in Python: How Python "really works"

  • -
  • Control flow: if / for / while / try, iterators, "tactical programming" tips

  • -
  • A brief tour through Python's standard library

  • -
  • Code design and best practices: How to design your code

  • -
-
- -
-

What is object-oriented programming?

-
-

Object-oriented programming does not mean using an object-oriented programming language.

-

It is rather a programming paradigm based on the concept of object, as well as on some general principles and best practices aiming at:

-
    -
  • improving readability

  • -
  • improving re-usability

  • -
  • improving modularity

  • -
  • providing foundation for a more intuitive design

  • -
-
- -
-

General principles

-
-

The following four concepts / principles are presented in most object-oriented programming books:

-
    -
  • separate and hide "private" details from the outside world and / or child (inheriting) functionality (encapsulation)

  • -
  • separate the interface from its implementation (abstraction)

  • -
  • inherit and extend / adapt existing functionality, through "is-a" relationship hierarchy (inheritance)

  • -
  • execute different code / functionality based on the object's place in the hierarchy (polymorphism)

  • -
-
- -
-

Building blocks and definitions

-
-

Note: "Lacking universally accepted terminology to talk about classes, I will make occasional use of Smalltalk and C++ terms." - The Python tutorial (https://docs.python.org)

-

When talking about object-oriented programming, the following building blocks are involved:

-
    -
  • class - a blueprint / template for creating objects

  • -
  • object - an instance of a class that may contain its own attributes as well as references to its class' attributes

  • -
  • attribute - variable, property, function defined in the class and present in its instances

  • -
  • class variable - attribute of which a single copy exists, regardless of how many instances of the class exist

  • -
  • object / instance variable - attribute for which each instantiated object of the class has a separate copy, or instance

  • -
  • method - member function - function that is an attribute

  • -
-
- -
-

The object-oriented world of Python

-
-

Some additional concepts and definitions are introduced in Python, as well as in some other programming languages:

-
    -
  • metaclass - a class whose instances are classes

  • -
  • property - attribute that provides a flexible mechanism to read, write, or compute the value of a "private" attribute

  • -
  • type - old C object-oriented term, now (in Python 3) considered to be the same as class

  • -
-

As is true for modules, classes partake of the dynamic nature of Python: they are created at runtime, and can be modified further after creation.

-
- -
-

The object-oriented world of Python (cont...)

-
-

The <

-
    -
  • metaclass - a class whose instances are classes

  • -
  • property - attribute that provides a flexible mechanism to read, write, or compute the value of a "private" attribute

  • -
  • type - old C object-oriented term, now (in Python 3) considered to be the same as class

  • -
-
- -
-
- - - - - - - - - - - diff --git a/reveal.js/python.html b/reveal.js/python.html deleted file mode 100644 index d27cd49..0000000 --- a/reveal.js/python.html +++ /dev/null @@ -1,680 +0,0 @@ - - - - - Introduction to Python - - - - - - - - - - - - - - - -
- - -
- -
-

Introduction to Python

-

Data Engineering @ Statnett

-
-

Simeon Simeonov

-
- -
-

Goals

-
-
    -
  • Present the Python programming language in a different way than https://docs.python.org

  • -
  • Avoid information overload

  • -
  • Use examples and interaction rather than documents and slides

  • -
-
- -
-

Preliminary plan

-
-
    -
  • Basics: About the language, the Python eco-system, types, modules, functions, scopes, decorators, string formatting

  • -
  • Object-oriented programming in Python: How Python "really works"

  • -
  • Control flow: if / for / while / try, iterators, "tactical programming" tips

  • -
  • A brief tour through Python's standard library

  • -
  • Code design and best practices: How to design your code

  • -
-
- -
-

What is Python?

-
-
    -
  • Python is an interpreted high-level general-purpose programming language - advanced through the Python Enhancement Proposal (PEP) process

  • -
  • CPython is the reference implementation of Python, written in C (alternatives: pypy, jython)

  • -
  • python - interpreter and interpreter shell (alternatives: ipython, bpython)

  • -
  • libpython

  • -
  • Calling C from Python: Cython, CFFI, ctypes

  • -
-
- -
-

Philosophy

-
-
-            
-              $ python
-            
-          
-
-            
-              import this
-
-              # The Zen of Python, by Tim Peters
-
-              # Beautiful is better than ugly.
-              # Explicit is better than implicit.
-              # Simple is better than complex.
-              # Complex is better than complicated.
-              # Flat is better than nested.
-              # Sparse is better than dense.
-              # Readability counts.
-              # Special cases aren't special enough to break the rules.
-              # Although practicality beats purity.
-              # Errors should never pass silently.
-              # Unless explicitly silenced.
-              # In the face of ambiguity, refuse the temptation to guess.
-              # There should be one-- and preferably only one --obvious way to do it.
-              # Although that way may not be obvious at first unless you're Dutch.
-              # Now is better than never.
-              # Although never is often better than *right* now.
-              # If the implementation is hard to explain, it's a bad idea.
-              # If the implementation is easy to explain, it may be a good idea.
-              # Namespaces are one honking great idea -- let's do more of those!
-            
-          
-
- -
-

Built-in functions

-
-

Few built-in functions.

-

https://docs.python.org/3/library/functions.html

-
    -
  • dir([obj]) - returns a list of valid attributes for that object
  • -
  • id(obj) - returns the "identity" of an object - an integer which is guaranteed to be unique
  • -
  • print(...) - prints objects to a text stream
  • -
  • str(...) - returns a string version of object
  • -
  • type(obj) - returns the type of an object
  • -
-
- -
-

Common built-in types

-
-

Python uses duck typing and has typed objects but untyped variable names.

-

Type constraints are not checked at compile time; rather, operations on an object may fail, signifying that the given object is not of a suitable type. Despite being dynamically-typed, Python is strongly-typed, forbidding operations that are not well-defined (for example, adding a number to a string) rather than silently attempting to make sense of them.

-
-            
-              s = 'foo'  # this is a string / str, same as str('foo'), may be encoded, immutable
-              b = b'foo'  # bytes, same as bytes('foo', 'utf-8'), may be decoded, immutable
-              i = 6  # int, same as int('6'), immutable
-              f = 0.1  # float, same as float('0.1'), immutable
-              b = False  # bool, same as bool(0), bool(''), bool(None)... immutable / constant
-              n = None  # NoneType, similar to 'null' in other languages, immutable / constant
-              l = [1, False, 'foo']  # list, same as list((1, False, 'foo'))
-              t = (1, False, 'foo')  # tuple, same as tuple([1, False, 'foo']), immutable
-              d = {'foo': 1, 'bar': 8}  # dict, same as dict(foo=1, bar=8), similar to hash in other languages
-              s = {'foo', 'bar', 1, 1, 4}  # set, same as set(['foo', 'bar', 1, 1, 4]), removes duplicates
-            
-          
-

Classes, functions, modules, .... and even types are simply other types :)

-
- -
-

Modules

-
-

A module is a file containing Python definitions and statements. The file name is the module name with the suffix .py appended. Within a module, the module's name (as a string) is available as the value of the global variable __name__

-

When a module named foo is imported, the interpreter first searches for a built-in module with that name (sys.builtin_module_names). If not found, it then searches for a file named foo.py in a list of directories given by the variable sys.path. sys.path is initialized from these locations:

-
    -
  • the directory containing the input script (or the current directory when no file is specified)
  • -
  • PYTHONPATH - env. variable - a list of directory names
  • -
  • the installation-dependent default locations
  • -
- -

The module is then imported only once and "cached" in sys.modules

-
- -
-

Packages

-
-

Packages are a way of structuring Python's module namespace by using "dotted module names"

-

The import statement combines two operations:

-
    -
  • it searches for the named module
  • -
  • it binds the results of that search to a name in the local scope
  • -
-
-            
-              # bar.py then bar/__init__.py will be considered, the first match executed and bound to 'bar'
-              import bar
-
-              import mymodule.foo  # implicitly executes mymodule.py, mymodule/__init__.py and mymodule/foo/__init__.py
-
-              import numpy as np  # will be bound as 'np' instead of 'numpy'. N.B. __name__ is still 'numpy'
-
-              import some.extremely.deep.path.Animal as Animal  # "sacrifice" the namespace in the name of convinience
-
-              from sys import path  # execute sys and only import the 'path' attribute into local scope as 'path'
-
-              # relative imports must be explicit in Python 3
-              from .othermodule import something  # expects that current module and 'othermodule' are in the same
-                                                  # package (containing __init__.py)
-
-              from sys import *  # NO! Bad programming practice since 1879
-            
-          
-
- -
-

Creating and maintaining a Python environment

-
-

Python's official package repository is PyPi (https://pypi.org), while Python's official package installer is pip (https://pypi.org/project/pip/)

-

A Python environment is the physical and logical arrangement of Python modules and packages. Several options exist:

-
    -
  • using a proper operating system :) (symlinks, real commercial support etc.)
  • -
  • using venv
  • -
  • using higher level tools like poetry
  • -
  • using a mixture / cocktail of all of the above :)
  • -
-
- -
-

Creating and maintaining a Python environment (cont...)

-
-

Desired qualities for a flexible Python environment:

-
    -
  • easy to create and (un)load
  • -
  • do not require extra privileges
  • -
  • don't repeat yourself (DRY)
  • -
  • easy to update without breaking the API
  • -
  • easy to debug
  • -
  • play nicely with the VCS (git)
  • -
-
- -
-

Creating and maintaining a Python environment (cont...)

-
-

Exploting the operating system can be done by:

-
    -
  • (re)defining PYTHONPATH
  • -
  • using symlinks to point at packages placed at different locations
  • -
- -
-

Creating and maintaining a Python environment (cont...)

-
-

Using venv can be done by directly invoking python:

-
-            
-              # create a virtual environment
-              python -m venv my_virtual_env
-              python -m venv --system-site-packages my_virtual_env
-
-              # load, use and unload the virtual environment
-              source my_virtual_env/bin/activate
-              pip install sqlalchemy
-              # install package from a custom repository (https://artifactory.fifty.eu)
-              pip install --index-url=https://artifactory.fifty.eu/artifactory/api/pypi/pypi/simple/ odin-data-access
-              deactivate
-
-              # one can alternatively use the python "wrapper" of the virtual env
-              my_virtual_env/bin/python -m pip install sqlalchemy
-            
-          
-

--system-site-packages will keep the original site-packages folders at the end of sys.path

-
- -
-

Creating and maintaining a Python environment (cont...)

-
-

Poetry (https://python-poetry.org) is the prefered environment and dependency management tool at Statnett.

-
-            
-              # create project and a virtual environment from scratch
-              poetry new my-project
-
-              # ... or use Poetry with an existing one
-              cd my-project
-              poetry init
-
-              # edit pyproject.toml for your needs (f.i. add dependencies, metadata ... etc),
-              # create virtual environment and install dependencies
-              poetry install
-              # finally commit your poetry.lock file to version control
-
-              # update all dependencies
-              poetry update
-            
-          
-

For more info: https://python-poetry.org/docs/basic-usage/

-
- -
-

Mutables vs. immutables

-

Immutable object is an object with a fixed value. Immutable objects include bool, int, float, str, bytes and tuples. Such an object cannot be altered. A new object has to be created if a different value has to be stored. They play an important role in places where a constant hash value is needed, for example as a key in a dictionary.

-

All objects that are not immutable are... mutable. All hashable objects should be immutable or use id().

-
-            
-              i = 1
-              id(i)  # returns f.i. 9788992
-              i += 1  # same as i = i + 1
-              id(i)  # returns a different value, hence - a brand new object
-
-              s = 'Hello'
-              s += ' World'  # s is now a different object
-              s[0]  # 'H'
-              s[0] = 'h'  # TypeError: 'str' object does not support item assignment
-
-              t = (1, 4)  # tuple
-              l = [1, 4]  # list
-              hash(t)  # returns f.i. -6333845781340707986
-              hash(l)  # TypeError: unhashable type: 'list'
-
-              s = 'the long and winding road'
-              s2 = 'the long and winding road'
-
-              # check if s and s2 are the same object:
-              id(s)  # Out: 139858905258704
-              id(s2)  # Out: 139858926037472
-
-              # the hash should be the same
-              hash(s)  # Out: 7030216208569256362
-              hash(s2)  # Out: 7030216208569256362
-            
-          
-
- -
-

Functions

-
-

A function is a sequence of program instructions that performs a specific task, packaged as a unit.

-

Functions let you:

-
    -
  • reuse code across several programs / projects
  • -
  • minimize code duplication
  • -
  • devide larger programming tasks
  • -
  • hide implementation details
  • -
  • improve readability
  • -
  • improve traceability
  • -
-

Function calls bring some overhead pushing / popping function-data into / from stack.

-

Important definitions (may have different meanings in different programming languages):

-
    -
  • parameter / formal parameter - variable / data provided as input to the function
  • -
  • argument / actual parameter - local variable / data to the given function
  • -
-

The keyword def introduces a function definition.

-
- -
-

Functions (cont...)

-
-
-            
-              def add(a, b):  # - function definition / header
-                  """Function for adding integers"""  # - docstring
-                  result = a + b
-                  a = 5
-                  return result  # - function that does not contain return, implicitly returns None
-
-              a_param = 9
-              b_param = -2
-              add(a_param, b_param)  # Out: 7
-
-              # integers are immutable and a_param will remain unchanged
-              print(a_param)  # Out: 9
-
-
-              def addl(a, b):
-                  """Function for adding two lists"""
-                  result = a + b
-                  a += [5]  # in this case equal to: a.append(5)
-                  return result
-
-              a_param = [9]
-              b_param = [-2]
-              addl(a_param, b_param)  # Out: [9, -2]
-              # lists are mutable and a_param will be changed
-              print(a_param)  # Out: [9, 5]
-            
-          
-
- -
-

Functions (cont ...)

-
-

Parameters and arguments

-
-            
-              def add(a, b):
-                  """Function for adding integers"""
-                  return a + b
-              my_result = add(2, 5)  # positional arguments (parameters)
-              my_result = add(b=5, a=2)  # keyword arguments (parameters)
-              my_tuple = (2, 5)
-              my_dict = {'b': 5, 'a': 2}
-              my_result = add(*my_tuple)  # unpacked and assigned to the positional arguments
-              my_result = add(**my_dict)  # unpacked and assigned to the kw. arguments
-
-              def add(a, b=5):
-                  """Function for adding integers"""
-                  return a + b
-              my_result = add(2)
-              # ... and the rest of the examples above will work
-
-              def add(a, *args, **kwargs):
-                  """Function for adding integers"""
-                  if args:
-                      b = args[0]
-                  elif 'b' in kwargs:
-                      b = kwargs['b']
-                  return a + b
-              my_result = add(2, 5, 9, 11)  # 5 assigned to args[0]
-              my_result = add(2, b=5, c=9, d=11)  # 5 assigned to kwargs['b']
-              my_result = add(2, b=5, 9, 11)  # SyntaxError: positional argument follows keyword argument
-            
-          
-
- -
-

Functions (cont ...)

-
-

Docstrings annotations and other hints

-
-            
-              def decrypt(password: str, edata: str) -> str:
-                  """
-                  Decrypts `edata` using `password`.
-
-                  `edata` is in the following format:
-                  enc-val$`version-num`$`bas64-salt`$`base64-encrypted_data`
-
-                  :param password: The password to generate the key with
-                  :type password: str
-
-                  :param edata: The data to be decrypted
-                  :type edata: str
-
-                  :raises EtoolkitInstanceError: If the encryption format is unsupported
-
-                  :return: The output string / decrypted data
-                  :rtype: str
-                  """
-                  if not edata.startswith('enc-val$1$'):
-                      raise EtoolkitInstanceError('Unsupported encryption format')
-                  # some more code magic coming after....
-                  # ...
-                  # ..
-                  return decrypted_str
-            
-          
-
- -
-

Functions (cont ...)

-
-

Using typing for more advanced / flexible hinting

-
-            
-              import typing
-
-              Basestring = typing.Union[str, bytes]
-
-              def decrypt(password: Basestring, edata: str) -> str:
-                  pass
-
-
-              from typing import Union
-
-              def decrypt(password: Union[str, bytes], edata: str) -> str:
-                  """Generic documentation. No need for pass"""
-
-              # Python >= 3.10 only
-              def decrypt(password: str | bytes, edata: str) -> str:
-                  """Generic documentation. No need for pass"""
-            
-          
-
- -
-

Functions (cont ...)

-

Functions as parameters / arguments, lambdas and returning multiple values

-
-            
-              def fetch_the_first_letter(input_str: str) -> str:
-                  """Fetches the first letter of the string input_str or 'x'""""
-                  try:
-                      return input_str[0]
-                  except Exception:
-                      return 'x'
-              letter_list = list(map(fetch_the_first_letter, ['foo', 'bar', 'test']))  # Out: ['f', 'b', 't']
-            
-          
-

Small anonymous functions can be created with the lambda keyword

-
-            
-              letter_list = list(map(lambda x: x[0], ['foo', 'bar', 'test']))  # Out: ['f', 'b', 't']
-            
-          
-

Functions in Python are callable objects. Callable objects can be created by defining the __call__ method. More on that later in the course...

-

A function can return multiple values by implicitly returning a tuple:

-
-            
-              def square_cube(x):
-                  """returns x, x^2 and x^3"""
-                  return x, x**2, x**3
-              numbers = square_cube(5)  # Out: (5, 25, 125)
-              num, sqnum, cbnum = square_cube(5)  # unpacking the tuple
-            
-          
-
- -
-

Functions (cont ...)

-
-

Enclosing and nested functions

-

Can be used as:

-
    -
  • regular functions within functions
  • -
  • dynamic function factories
  • -
-
-            
-              def get_multiplier_of(base: int) -> str:
-                  """the function enclosing its nested functions"""
-    
-                  def multiplier_function(x):
-                      """a nested function"""
-                      return base * x
-
-                  return multiplier_function
-
-              times3 = get_multiplier_of(3)
-              times5 = get_multiplier_of(5)
-              print(times3(3))  # Out: 9
-              print(times5(3))  # Out: 15
-            
-          
-
- -
-

Scopes in Python

-
-
    -
  • local - assigned names are local unless declared global
  • -
  • enclosed - the scope of the variable inside a function with a nested function
  • -
  • global - global for the current module
  • -
  • built-in
  • -
-

locals() and globals() return dicts of symbols for their respective scopes

-
-            
-              num1, num2 = 7, 8  # module globals
-
-              def print_numbers():
-                  print(num1, num2)  # OK, these are module globals
-                  num3 = 100
-                  print(num3)  # prints 100, num3 is in the function (local) scope
-                  global num4  # assignes / references num4 to / in the global scope
-                  num4 = 99
-                  id = 200  # new symbol in local scope
-                  # id(num4)  # will not yield the expected result (raises TypeError)
-
-                  def print_numbers2():
-                      print(num3)  # OK, enclosed scope
-
-                  print_numbers2()  # prints 100
-
-              print_numbers()
-              # print(num3)  # Raises NameError - why?
-              print(num4)  # Prints 99 - why?
-              # print_numbers2()  # Raises NameError
-            
-          
-
- -
-

Decorators

-

Decorators can be used to modify the behavior of the objects they decorate. Decorators can be implemented either by using classes or by using nested functions.

-
-            
-              def my_decorator(func):
-
-                  def decorated():
-                      print('Doing something before the decorated function')
-                      retval = func()
-                      print('Doing something after the decorated function')
-                      return retval
-                  return decorated
-
-              def my_function():
-                  print('Alice')
-
-              my_function = my_decorator(my_function)
-              my_function()
-            
-          
-

... may be dificult to read / understand, while:

-
-            
-              @my_decorator
-              def my_function():
-                  print('Alice')
-
-              my_function()
-            
-          
-

... may be easier

-
- -
-

Decorators (cont ...)

-
-

A complete example

-
-            
-              import sys
-              from functools import wraps
-
-              def requires_access(access_secret: str):
-
-                  def api_access_decorator(f):
-
-                      @wraps(f)
-                      def decorated(*args, **kwargs):
-                          if 'secret' not in kwargs:
-                              sys.exit('No secret provided')
-                          if not kwargs['secret'] or kwargs['secret'] != access_secret:
-                              sys.exit("Secret doesn't match")
-                          # return f(args[0], **kwargs)
-                          return f(*args, **kwargs)
-
-                      return decorated
-
-                  return api_access_decorator
-
-
-              @requires_access(access_secret='b28cfeaa65b73cf')
-              def sensitive_function(data, **kwargs):
-                  """very sensitive function"""
-                  db.save(data)
-            
-          
-
- -
-

String formatting

-
-

The old ways...

-
-            
-              f = 6.57865
-              i = 27
-              s = 'another string'
-
-
-              '%s - %d - %5.2f' % (s, i, f)  # Out: 'another string - 27 -  6.58'
-
-              # still used in:
-              logger.debug("%d - %s", event.id, message)
-
-
-              '{} - {} - {:5.2f}'.format(s, i, f)  # implicit
-              '{0} - {1} - {2:5.2f}'.format(s, i, f)  # explicit
-              '{my_str} - {i} - {fl:5.2f}'.format(my_str=s, fl=f, i=i)  # keyword
-              # Out: 'another string - 27 -  6.58'
-
-
-              # modern Python >= 3.6 f-strings
-              f'{s} - {i} - {f:5.2f}'  # Out: 'another string - 27 -  6.58'
-            
-          
-

See https://docs.python.org/3/library/string.html#formatspec for the complete format specification

-
- -
-
- - - - - - - - - - - diff --git a/reveal.js/rabbitmq.html b/reveal.js/rabbitmq.html index ed1cbfd..6d1ba7a 100644 --- a/reveal.js/rabbitmq.html +++ b/reveal.js/rabbitmq.html @@ -1,311 +1,311 @@ - - - RabbitMQ - Intern opplæring - - - - + + + RabbitMQ - Intern opplæring + + + + - - + + - + - - - - -
+ + + + +
- -
-
-

RabbitMQ

-

Intern opplæring

+ +
+
+

RabbitMQ

+

Intern opplæring


-

- Simeon Simeonov -

-
+

+ Simeon Simeonov +

+
-
-
-

Agenda

+
+
+

Agenda


    -
  • Installasjon
  • -
  • Administrasjon
  • -
  • Utvikling
  • -
  • Diverse + Q&A
  • +
  • Installasjon
  • +
  • Administrasjon
  • +
  • Utvikling
  • +
  • Diverse + Q&A
-
-
+
+
-
-
-

Installasjon og oppsett

+
+
+

Installasjon og oppsett


    -
  • Skaffe seg maskin og SSL sertifikat(er)

  • -
  • Bruke Ansible

  • -
  • Opprette administrator-bruker og slette guest-brukeren

  • -
  • Sette rettigheter og opprette entiteter

  • +
  • Skaffe seg maskin og SSL sertifikat(er)

  • +
  • Bruke Ansible

  • +
  • Opprette administrator-bruker og slette guest-brukeren

  • +
  • Sette rettigheter og opprette entiteter

-
-
-

Ansible

+
+
+

Ansible


-

Vi bruker repo UAIT/int-ansible-hosts

-

Etter å ha installert Ansible på lokalmaskina gjør vi følgende:

-
    -
  • git clone ssh://git@bitbucket.usit.uio.no:7999/uait/int-ansible-hosts.git

  • -
  • cd int-ansible-hosts

  • -
  • ansible-playbook --ask-become-pass -v --extra-vars '{"hosts": "mq-hostname"}' rabbitmq_playbook.yml

  • -
-

RabbitMQ skal nå være installert og tilgjengelig på https://mq-hostname

-
+

Vi bruker repo UAIT/int-ansible-hosts

+

Etter å ha installert Ansible på lokalmaskina gjør vi følgende:

+
    +
  • git clone ssh://git@bitbucket.usit.uio.no:7999/uait/int-ansible-hosts.git

  • +
  • cd int-ansible-hosts

  • +
  • ansible-playbook --ask-become-pass -v --extra-vars '{"hosts": "mq-hostname"}' rabbitmq_playbook.yml

  • +
+

RabbitMQ skal nå være installert og tilgjengelig på https://mq-hostname

+
-
-

Administrator-bruker

+
+

Administrator-bruker


-

Brukeren guest med passord guest vil eksistere etter at Ansible har kjørt

-

I tillegg til webgrensesnittet, kan en bruke rabbitmqctl

-
    -
  • rabbitmqctl list_users (Lister alle eksisterende RabbitMQ-brukere)

  • -
  • rabbitmqctl add_user rmq_admin <passord> (Legger til dedikert admin bruker rmq_admin)

  • -
  • rabbitmqctl set_user_tags rmq_admin administrator (Gir administrator rolle til rmq_admin)

  • -
  • rabbitmqctl delete_user guest (Sletter brukeren guest)

  • -
-
+

Brukeren guest med passord guest vil eksistere etter at Ansible har kjørt

+

I tillegg til webgrensesnittet, kan en bruke rabbitmqctl

+
    +
  • rabbitmqctl list_users (Lister alle eksisterende RabbitMQ-brukere)

  • +
  • rabbitmqctl add_user rmq_admin <passord> (Legger til dedikert admin bruker rmq_admin)

  • +
  • rabbitmqctl set_user_tags rmq_admin administrator (Gir administrator rolle til rmq_admin)

  • +
  • rabbitmqctl delete_user guest (Sletter brukeren guest)

  • +
+
-
-

vhost

+
+

vhost


-

vhost fungerer som en beholder (container) for RabbitMQ/AMQP objekter. Nok igjen kan en velge mellom webgrensesnitt og rabbitmqctl

-
    -
  • rabbitmqctl list_vhosts (Lister alle eksisterende vhosts)

  • -
  • rabbitmqctl add_vhost /no/uio/integration (Oppretter vhost med navn /no/uio/integration)

  • -
-
+

vhost fungerer som en beholder (container) for RabbitMQ/AMQP objekter. Nok igjen kan en velge mellom webgrensesnitt og rabbitmqctl

+
    +
  • rabbitmqctl list_vhosts (Lister alle eksisterende vhosts)

  • +
  • rabbitmqctl add_vhost /no/uio/integration (Oppretter vhost med navn /no/uio/integration)

  • +
+
-
-

Rettigheter

-

RabbitMQ implementerer 2 rettighetsnivåer(*):

-
    -
  • Per vhost
  • -
  • Per entitet
  • -
-

RabbitMQ definerer 3 typer operasjoner:

-
    -
  • configure - opprette / slette entiteter eller endre deres oppførsel

  • -
  • write - skrive melding til en entitet

  • -
  • read - lese melding fra entitet

  • -
-

(*) - /etc/rabbitmq/rabbitmq.config gir flere muligheter

-
+
+

Rettigheter

+

RabbitMQ implementerer 2 rettighetsnivåer(*):

+
    +
  • Per vhost
  • +
  • Per entitet
  • +
+

RabbitMQ definerer 3 typer operasjoner:

+
    +
  • configure - opprette / slette entiteter eller endre deres oppførsel

  • +
  • write - skrive melding til en entitet

  • +
  • read - lese melding fra entitet

  • +
+

(*) - /etc/rabbitmq/rabbitmq.config gir flere muligheter

+
-
-

Rettigheter og brukere

-

RabbitMQ bruker regular expressions til å definere rettigheter

-
    -
  • rabbitmqctl add_user cerebrum <passord>

  • -
  • rabbitmqctl add_user uio_ad_microservice <passord>

  • -
  • rabbitmqctl set_permissions -p /no/uio/integration cerebrum "^$" "^ex_.*" "^$"

  • -
  • rabbitmqctl set_permissions -p /no/uio/integration uio_ad_microservice "^q_ad_ms_.*" "^q_ad_ms_.*" "^(ex_messages|q_ad_ms_.*)$"

  • -
-
+
+

Rettigheter og brukere

+

RabbitMQ bruker regular expressions til å definere rettigheter

+
    +
  • rabbitmqctl add_user cerebrum <passord>

  • +
  • rabbitmqctl add_user uio_ad_microservice <passord>

  • +
  • rabbitmqctl set_permissions -p /no/uio/integration cerebrum "^$" "^ex_.*" "^$"

  • +
  • rabbitmqctl set_permissions -p /no/uio/integration uio_ad_microservice "^q_ad_ms_.*" "^q_ad_ms_.*" "^(ex_messages|q_ad_ms_.*)$"

  • +
+
-
-

Køer

-

Køer defineres som durable ved hjelp av webgrensesnittet.

-

"Alle køer (både forhåndsdefinerte og de som defineres av konsument) må ha navn som starter med q_. Køen vil ha navn som uttryker mottakeren som bruker den."

-

F.eks. q_ad_ms_all vil være et passende navn for køen som brukes av AD-microservice mottakeren.

-
+
+

Køer

+

Køer defineres som durable ved hjelp av webgrensesnittet.

+

"Alle køer (både forhåndsdefinerte og de som defineres av konsument) må ha navn som starter med q_. Køen vil ha navn som uttryker mottakeren som bruker den."

+

F.eks. q_ad_ms_all vil være et passende navn for køen som brukes av AD-microservice mottakeren.

+
-
-

Exchange

-

"Avsendere vil vanligvis sende meldinger til exchange ex_messages."

-

Det er flere typer exchange, men vi vil bruke kun topic exchange.

-
-
+
+

Exchange

+

"Avsendere vil vanligvis sende meldinger til exchange ex_messages."

+

Det er flere typer exchange, men vi vil bruke kun topic exchange.

+
+
-
-
-

Utvikling

+
+
+

Utvikling


    -
  • Routing keys i topic exchange

  • -
  • Bindinger

  • -
  • Protokoller og porter

  • -
  • Eksempler

  • +
  • Routing keys i topic exchange

  • +
  • Bindinger

  • +
  • Protokoller og porter

  • +
  • Eksempler

-
+
-
-

Routing keys i topic exchange

+
+

Routing keys i topic exchange


-

Routing key settes av sender som en del av meldingen og blir inspisert av brokeren dersom meldingen sendes til en topic exchange.

-

Strukturen til en topic / message routing key er:

-

<kilde>.<type>.<objekt>.<hendelse>

-

F.eks.:

-

cerebrum.event.person.delete

-
+

Routing key settes av sender som en del av meldingen og blir inspisert av brokeren dersom meldingen sendes til en topic exchange.

+

Strukturen til en topic / message routing key er:

+

<kilde>.<type>.<objekt>.<hendelse>

+

F.eks.:

+

cerebrum.event.person.delete

+
-
-

Bindinger

-

For at en melding sendt til en topic exchange skal havne i en bestemt kø, må køen være bundet (bound) til exchange.

-

I topic exchange brukes routing key (topic) til å avgjøre hvilke av meldingene som blir sendt vil havne i køen som er bundet.

-

Dersom vi binder køen q_ad_ms_all til topic exchange ex_messages med binding key cerebrum.event.account.* vil alle meldinger som blir sendt til ex_messages med topic som starter med cerebrum.event.account. havne i q_ad_ms_all.

-

En kø kan bli bundet med én eller flere nøkler til en topic exchange.

-
+
+

Bindinger

+

For at en melding sendt til en topic exchange skal havne i en bestemt kø, må køen være bundet (bound) til exchange.

+

I topic exchange brukes routing key (topic) til å avgjøre hvilke av meldingene som blir sendt vil havne i køen som er bundet.

+

Dersom vi binder køen q_ad_ms_all til topic exchange ex_messages med binding key cerebrum.event.account.* vil alle meldinger som blir sendt til ex_messages med topic som starter med cerebrum.event.account. havne i q_ad_ms_all.

+

En kø kan bli bundet med én eller flere nøkler til en topic exchange.

+
-
-

Protokoller og porter

+
+

Protokoller og porter


-

"AMQP 0.9.1 prioritert siden det er protokollen som mapper best mot RabbitMQs funksjonalitet. Vi har også noe erfaring med bruk av denne."

-

RabbitMQ lytter for AMQP0.9.1 på SSL port 5671.

+

"AMQP 0.9.1 prioritert siden det er protokollen som mapper best mot RabbitMQs funksjonalitet. Vi har også noe erfaring med bruk av denne."

+

RabbitMQ lytter for AMQP0.9.1 på SSL port 5671.


-

Andre protokoller og tjenester for RabbitMQ:

-
    -
  • STOMP - 61614 (SSL)
  • -
  • RabbitMQ Management (webgrensesnitt) - 15671 (SSL)
  • -
-
+

Andre protokoller og tjenester for RabbitMQ:

+
    +
  • STOMP - 61614 (SSL)
  • +
  • RabbitMQ Management (webgrensesnitt) - 15671 (SSL)
  • +
+
-
-

Eksempler

+
+

Eksempler


-              
-                $ pip install pika
-              
+                
+                    $ pip install pika
+                
             
-

Python:

-

"Well, we'll not risk another frontal assault. That rabbit's dynamite...."

-
+

Python:

+

"Well, we'll not risk another frontal assault. That rabbit's dynamite...."

+
-
+
-
-
-

Dokumentasjon og lenker

+
+
+

Dokumentasjon og lenker


-

- RabbitMQ: -

- -
-
-

Dokumentasjon og lenker

-

- UiO: -

- -

- Bøker: -

-
    -
  • -

    - - RabbitMQ in action - Alvaro Videla / Jason J.W. Williams - 2012 - Manning - -

  • -
  • -

    - - Mastering RabbitMQ - Ayanoglu / Aytas / Nahum - 2015 - PACKT Publishing - -

  • -
-
+

+ RabbitMQ: +

+ +
+
+

Dokumentasjon og lenker

+

+ UiO: +

+ +

+ Bøker: +

+
    +
  • +

    + + RabbitMQ in action - Alvaro Videla / Jason J.W. Williams - 2012 - Manning + +

  • +
  • +

    + + Mastering RabbitMQ - Ayanoglu / Aytas / Nahum - 2015 - PACKT Publishing + +

  • +
+
+ +
+

Q&A

+
+ +
+
-
-

Q&A

-
+ + + + + + + - - - - - - + - + diff --git a/reveal.js/sqlalchemy.html b/reveal.js/sqlalchemy.html old mode 100755 new mode 100644 index a0d6617..bbcd1ca --- a/reveal.js/sqlalchemy.html +++ b/reveal.js/sqlalchemy.html @@ -98,29 +98,29 @@ metadata = MetaData() production_types_table = Table( - "production_types", - metadata, - Column("production_type_id", Integer, primary_key=True), - Column("code", String(3), nullable=False, unique=True), - Column("description", String), # Column("name", String(128)) is possible + "production_types", + metadata, + Column("production_type_id", Integer, primary_key=True), + Column("code", String(3), nullable=False, unique=True), + Column("description", String), # Column("name", String(128)) is possible ) bidding_areas_table = Table( - "bidding_areas", - metadata, - Column("bidding_area_id", Integer, primary_key=True), - Column("code", String(3), nullable=False, unique=True), - Column("name", String(32)), + "bidding_areas", + metadata, + Column("bidding_area_id", Integer, primary_key=True), + Column("code", String(3), nullable=False, unique=True), + Column("name", String(32)), ) production_plans_table = Table( - "production_plans", - metadata, - Column("record_created_time", DateTime(timezone=False), primary_key=True), - Column("start_time", DateTime(timezone=False), primary_key=True), - Column("bidding_area_id", Integer, ForeignKey("bidding_areas.bidding_area_id"), primary_key=True), - Column("production_type_id", Integer, ForeignKey("production_types.production_type_id"), primary_key=True), - Column("value", Numeric, nullable=False), + "production_plans", + metadata, + Column("record_created_time", DateTime(timezone=False), primary_key=True), + Column("start_time", DateTime(timezone=False), primary_key=True), + Column("bidding_area_id", Integer, ForeignKey("bidding_areas.bidding_area_id"), primary_key=True), + Column("production_type_id", Integer, ForeignKey("production_types.production_type_id"), primary_key=True), + Column("value", Numeric, nullable=False), ) metadata.create_all(engine) # creates the tables @@ -150,13 +150,13 @@ insert_stmt.execute(bidding_area_id=1, code="NO1", name="Elspot NO1") # insert a single entry # ... or a list of entries insert_stmt.execute( - [ - {"bidding_area_id": 2, "code": "NO2", "name": "Elspot NO2"}, - {"bidding_area_id": 3, "code": "NO3", "name": "Elspot NO3"}, - {"bidding_area_id": 4, "code": "NO4", "name": "Elspot NO4"}, - {"bidding_area_id": 5, "code": "NO5", "name": "Elspot NO5"}, - {"bidding_area_id": 6, "code": "NO6", "name": "Elspot NO6"}, - ] + [ + {"bidding_area_id": 2, "code": "NO2", "name": "Elspot NO2"}, + {"bidding_area_id": 3, "code": "NO3", "name": "Elspot NO3"}, + {"bidding_area_id": 4, "code": "NO4", "name": "Elspot NO4"}, + {"bidding_area_id": 5, "code": "NO5", "name": "Elspot NO5"}, + {"bidding_area_id": 6, "code": "NO6", "name": "Elspot NO6"}, + ] ) metadata.bind = engine # no need to explicitly bind the engine from now on @@ -182,21 +182,21 @@ from sqlalchemy.orm import mapper class ProductionType: - def __init__(self, code, description): - self.code = code - self.description = description + def __init__(self, code, description): + self.code = code + self.description = description - def __str__(self): - return self.code + def __str__(self): + return self.code class BiddingArea: - def __init__(self, code, name): - self.code = code - self.name = name + def __init__(self, code, name): + self.code = code + self.name = name - def __str__(self): - return self.code + def __str__(self): + return self.code mapper(ProductionType, production_types_table) mapper(BiddingArea, bidding_areas_table) @@ -212,27 +212,27 @@ from sqlalchemy.orm import relationship class ProductionPlan: - def __init__(self, record_created_time, start_time, production_type, bidding_area, value): - self.record_created_time = record_created_time - self.start_time = start_time - self.production_type = production_type - self.bidding_area = bidding_area - self.value = value - - def __str__(self): - return ( - f"{self.record_created_time} {self.start_time} " - f"{self.production_type} {self.bidding_area} {self.value}" - ) + def __init__(self, record_created_time, start_time, production_type, bidding_area, value): + self.record_created_time = record_created_time + self.start_time = start_time + self.production_type = production_type + self.bidding_area = bidding_area + self.value = value + + def __str__(self): + return ( + f"{self.record_created_time} {self.start_time} " + f"{self.production_type} {self.bidding_area} {self.value}" + ) mapper( - ProductionPlan, - production_plans_table, - properties = { - "production_type": relationship(ProductionType, backref="production_plans"), - "bidding_area": relationship(BiddingArea, backref="production_plans"), - }, + ProductionPlan, + production_plans_table, + properties = { + "production_type": relationship(ProductionType, backref="production_plans"), + "bidding_area": relationship(BiddingArea, backref="production_plans"), + }, )
@@ -249,32 +249,32 @@ Base = declarative_base() class ProductionType(Base): - __tablename__ = "production_types" + __tablename__ = "production_types" - production_type_id = Column(Integer, primary_key=True) - code = Column(String(3), nullable=False, unique=True) - description = Column(String) + production_type_id = Column(Integer, primary_key=True) + code = Column(String(3), nullable=False, unique=True) + description = Column(String) - def __init__(self, code, description): - self.code = code - self.description = description + def __init__(self, code, description): + self.code = code + self.description = description - def __str__(self): - return self.code + def __str__(self): + return self.code class BiddingArea(Base): - __tablename__ = "bidding_areas" + __tablename__ = "bidding_areas" - bidding_area_id = Column(Integer, primary_key=True) - code = Column(String(3), nullable=False, unique=True) - name = Column(String(32)) + bidding_area_id = Column(Integer, primary_key=True) + code = Column(String(3), nullable=False, unique=True) + name = Column(String(32)) - def __init__(self, code, name): - self.code = code - self.name = name + def __init__(self, code, name): + self.code = code + self.name = name - def __str__(self): - return self.code + def __str__(self): + return self.code
@@ -288,31 +288,31 @@ from sqlalchemy.orm import relationship, backref class ProductionPlan(Base): - __tablename__ = "production_plans" - - record_created_time = Column(DateTime(timezone=False), primary_key=True) - start_time = Column(DateTime(timezone=False), primary_key=True) - bidding_area_id = Column(Integer, ForeignKey("bidding_areas.bidding_area_id"), primary_key=True) - production_type_id = Column(Integer, ForeignKey("production_types.production_type_id"), primary_key=True) - value = Column(Numeric, nullable=False) - - # defining relationships. - # the defined attributes will reference 'ProductionType' and 'BiddingArea' objects - production_type = relationship(ProductionType, backref=backref("production_plans")) - bidding_area = relationship(BiddingArea, backref=backref("production_plans")) - - def __init__(self, record_created_time, start_time, production_type, bidding_area, value): - self.record_created_time = record_created_time - self.start_time = start_time - self.production_type = production_type # a 'ProductionType' object - self.bidding_area = bidding_area # a 'BiddingArea' object - self.value = value - - def __str__(self): - return ( - f"{self.record_created_time} {self.start_time} " - f"{self.production_type} {self.bidding_area} {self.value}" - ) + __tablename__ = "production_plans" + + record_created_time = Column(DateTime(timezone=False), primary_key=True) + start_time = Column(DateTime(timezone=False), primary_key=True) + bidding_area_id = Column(Integer, ForeignKey("bidding_areas.bidding_area_id"), primary_key=True) + production_type_id = Column(Integer, ForeignKey("production_types.production_type_id"), primary_key=True) + value = Column(Numeric, nullable=False) + + # defining relationships. + # the defined attributes will reference 'ProductionType' and 'BiddingArea' objects + production_type = relationship(ProductionType, backref=backref("production_plans")) + bidding_area = relationship(BiddingArea, backref=backref("production_plans")) + + def __init__(self, record_created_time, start_time, production_type, bidding_area, value): + self.record_created_time = record_created_time + self.start_time = start_time + self.production_type = production_type # a 'ProductionType' object + self.bidding_area = bidding_area # a 'BiddingArea' object + self.value = value + + def __str__(self): + return ( + f"{self.record_created_time} {self.start_time} " + f"{self.production_type} {self.bidding_area} {self.value}" + ) Base.metadata.create_all(engine) # create tables @@ -337,27 +337,27 @@ session.add(bidding_area1) session.add_all( - [ - BiddingArea("NO2", "Elspot NO2"), - BiddingArea("NO3", "Elspot NO3"), - BiddingArea("NO4", "Elspot NO4"), - BiddingArea("NO5", "Elspot NO5"), - ] + [ + BiddingArea("NO2", "Elspot NO2"), + BiddingArea("NO3", "Elspot NO3"), + BiddingArea("NO4", "Elspot NO4"), + BiddingArea("NO5", "Elspot NO5"), + ] ) production_type_B37 = ProductionType("B37", "Thermal unspecified") production_type_B30 = ProductionType("B30", "Wind unspecified") session.add_all( - [ - ProductionType("B19", "Wind Onshore"), - ProductionType("B10", "Hydro-electric pure pumped storage head installation"), - ProductionType("B11", "Hydro Run-of-river head installation"), - ProductionType("B12", "Hydro-electric storage head installation"), - ProductionType("A04", "Generation"), - production_type_B37, - production_type_B30, - ] + [ + ProductionType("B19", "Wind Onshore"), + ProductionType("B10", "Hydro-electric pure pumped storage head installation"), + ProductionType("B11", "Hydro Run-of-river head installation"), + ProductionType("B12", "Hydro-electric storage head installation"), + ProductionType("A04", "Generation"), + production_type_B37, + production_type_B30, + ] ) @@ -371,21 +371,21 @@ # adding some production plans... session.add( - ProductionPlan( - datetime.datetime.now(), - datetime.datetime(2022, 11, 2, 1, 0), - production_type_B37, - bidding_area1, - decimal.Decimal("80.5"), - ) + ProductionPlan( + datetime.datetime.now(), + datetime.datetime(2022, 11, 2, 1, 0), + production_type_B37, + bidding_area1, + decimal.Decimal("80.5"), + ) ) production_plan2 = ProductionPlan( - datetime.datetime.now(), - datetime.datetime(2022, 11, 2, 2, 0), - production_type_B37, - bidding_area1, - decimal.Decimal("90.5"), + datetime.datetime.now(), + datetime.datetime(2022, 11, 2, 2, 0), + production_type_B37, + bidding_area1, + decimal.Decimal("90.5"), ) session.add(production_plan2) @@ -427,17 +427,17 @@ # return production plans with production type 'B37' session.query(ProductionPlan).filter( - ProductionPlan.production_type_id == ProductionType.production_type_id + ProductionPlan.production_type_id == ProductionType.production_type_id ).filter(ProductionType.code == "B37").all() session.query(ProductionPlan).join(ProductionType).filter(ProductionType.code == "B37").all() session.query(ProductionPlan).filter(ProductionPlan.production_type == production_type_B37).all() session.query( - ProductionPlan + ProductionPlan ).from_statement( - text( - "SELECT pp.* FROM production_plans pp, production_types pt " - "WHERE pp.production_type_id = pt.production_type_id AND pt.code=:code" - ) + text( + "SELECT pp.* FROM production_plans pp, production_types pt " + "WHERE pp.production_type_id = pt.production_type_id AND pt.code=:code" + ) ).params(code="B37").all() @@ -449,29 +449,28 @@ - - - - - - - - + + + + + + + + diff --git a/reveal.js/timetravel.html b/reveal.js/timetravel.html index e073f92..b297e2f 100644 --- a/reveal.js/timetravel.html +++ b/reveal.js/timetravel.html @@ -1,127 +1,127 @@ - - - Solving problems using time travel - - - - - - - - - - - - - - - -
- - -
- -
-

Solving problems using time travel

-

Beryl @ Fifty

+ + + Solving problems using time travel + + + + + + + + + + + + + + + +
+ + +
+ +
+

Solving problems using time travel

+

Beryl @ Fifty


-

Tomas Robertson (team ACE-OL) & Simeon Simeonov (team Forecasts)

-
- - -
-

What is time travel???

-
-

Time travel - our ability to look at our input data at the state it was at a specific point in time (not only at its last state).

-

Time travel is achieved by adding the record_created_time column to our DB tables and storing Kafka's created_time value (converted to UTC).

-
-

The main concepts around how and why were presented by Peter Sandberg.

-
- -
-

Solving problems

-
-

The following spike was observed 2021-10-06 around 10:15 CET @ NO4

- -
- -
-

Solving problems (cont)

-
-

No spikes shown in Grafana

- -
- -
-

Solving problems with time travel

-
-
-            
-              SELECT record_created_time, start_time, value
-              FROM misc.app_odin_ace_ol_ba_10s_avro_v01
-              WHERE bidding_area_name = 'NO4' AND start_time = '2021-10-06 08:14:10'
-              ORDER BY record_created_time;
-            
-          
- - - - - - - - - - - - - - - - - - - - - - - - - - -
record_created_timestart_timevalue
2021-10-06 08:15:52.2582021-10-06 08:14:10-370.37683609008127
2021-10-06 08:19:06.2222021-10-06 08:14:1025.183745117193457
2021-10-06 08:21:56.2072021-10-06 08:14:1034.78731922151518
-
- -
-

Q & A

-
- -
-
- - - - - - - - - - +

Tomas Robertson (team ACE-OL) & Simeon Simeonov (team Forecasts)

+
+ + +
+

What is time travel???

+
+

Time travel - our ability to look at our input data at the state it was at a specific point in time (not only at its last state).

+

Time travel is achieved by adding the record_created_time column to our DB tables and storing Kafka's created_time value (converted to UTC).

+
+

The main concepts around how and why were presented by Peter Sandberg.

+
+ +
+

Solving problems

+
+

The following spike was observed 2021-10-06 around 10:15 CET @ NO4

+ +
+ +
+

Solving problems (cont)

+
+

No spikes shown in Grafana

+ +
+ +
+

Solving problems with time travel

+
+
+                        
+                            SELECT record_created_time, start_time, value
+                            FROM misc.app_odin_ace_ol_ba_10s_avro_v01
+                            WHERE bidding_area_name = 'NO4' AND start_time = '2021-10-06 08:14:10'
+                            ORDER BY record_created_time;
+                        
+                    
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
record_created_timestart_timevalue
2021-10-06 08:15:52.2582021-10-06 08:14:10-370.37683609008127
2021-10-06 08:19:06.2222021-10-06 08:14:1025.183745117193457
2021-10-06 08:21:56.2072021-10-06 08:14:1034.78731922151518
+
+ +
+

Q & A

+
+ +
+
+ + + + + + + + + + -- cgit v1.3