From f3ec53fef73e9bb2d8c11836753fa80ed0abbbef Mon Sep 17 00:00:00 2001 From: <> Date: Thu, 6 Jul 2023 14:45:29 +0000 Subject: [PATCH] Deployed ee9bce5 with MkDocs version: 1.4.3 --- .nojekyll | 0 404.html | 1018 +++ apps/activities/index.html | 1166 +++ apps/admissions/index.html | 1076 +++ apps/monitoring/introduction/index.html | 1113 +++ apps/monitoring/scripts/index.html | 1243 +++ assets/_mkdocstrings.css | 36 + assets/images/favicon.png | Bin 0 -> 1870 bytes assets/javascripts/bundle.c2be25ad.min.js | 29 + assets/javascripts/bundle.c2be25ad.min.js.map | 8 + assets/javascripts/lunr/min/lunr.ar.min.js | 1 + assets/javascripts/lunr/min/lunr.da.min.js | 18 + assets/javascripts/lunr/min/lunr.de.min.js | 18 + assets/javascripts/lunr/min/lunr.du.min.js | 18 + assets/javascripts/lunr/min/lunr.es.min.js | 18 + assets/javascripts/lunr/min/lunr.fi.min.js | 18 + assets/javascripts/lunr/min/lunr.fr.min.js | 18 + assets/javascripts/lunr/min/lunr.hi.min.js | 1 + assets/javascripts/lunr/min/lunr.hu.min.js | 18 + assets/javascripts/lunr/min/lunr.hy.min.js | 1 + assets/javascripts/lunr/min/lunr.it.min.js | 18 + assets/javascripts/lunr/min/lunr.ja.min.js | 1 + assets/javascripts/lunr/min/lunr.jp.min.js | 1 + assets/javascripts/lunr/min/lunr.kn.min.js | 1 + assets/javascripts/lunr/min/lunr.ko.min.js | 1 + assets/javascripts/lunr/min/lunr.multi.min.js | 1 + assets/javascripts/lunr/min/lunr.nl.min.js | 18 + assets/javascripts/lunr/min/lunr.no.min.js | 18 + assets/javascripts/lunr/min/lunr.pt.min.js | 18 + assets/javascripts/lunr/min/lunr.ro.min.js | 18 + assets/javascripts/lunr/min/lunr.ru.min.js | 18 + assets/javascripts/lunr/min/lunr.sa.min.js | 1 + .../lunr/min/lunr.stemmer.support.min.js | 1 + assets/javascripts/lunr/min/lunr.sv.min.js | 18 + assets/javascripts/lunr/min/lunr.ta.min.js | 1 + assets/javascripts/lunr/min/lunr.te.min.js | 1 + assets/javascripts/lunr/min/lunr.th.min.js | 1 + assets/javascripts/lunr/min/lunr.tr.min.js | 18 + assets/javascripts/lunr/min/lunr.vi.min.js | 1 + assets/javascripts/lunr/min/lunr.zh.min.js | 1 + assets/javascripts/lunr/tinyseg.js | 206 + assets/javascripts/lunr/wordcut.js | 6708 +++++++++++++++++ .../workers/search.208ed371.min.js | 42 + .../workers/search.208ed371.min.js.map | 8 + assets/stylesheets/main.85bb2934.min.css | 1 + assets/stylesheets/main.85bb2934.min.css.map | 1 + assets/stylesheets/palette.a6bdf11c.min.css | 1 + .../stylesheets/palette.a6bdf11c.min.css.map | 1 + .../configuring-the-github-secrets/index.html | 1059 +++ deployment/environment-variables/index.html | 1233 +++ endponts/index.html | 1039 +++ images/dockerhub.PNG | Bin 0 -> 127183 bytes images/github-secrets.PNG | Bin 0 -> 82390 bytes index.html | 1303 ++++ installation/environment-variables/index.html | 1137 +++ installation/fixtures/index.html | 1118 +++ objects.inv | Bin 0 -> 735 bytes search/search_index.json | 1 + security/capabilities/index.html | 1414 ++++ .../google-cloud-functions/index.html | 1190 +++ services/google_cloud/storage/index.html | 1346 ++++ services/slack integration/icons/index.html | 1054 +++ signals/quickstart/index.html | 1190 +++ sitemap.xml | 138 + sitemap.xml.gz | Bin 0 -> 494 bytes testing/mixins/bc-cache/index.html | 1194 +++ testing/mixins/bc-check/index.html | 2214 ++++++ testing/mixins/bc-database/index.html | 3365 +++++++++ testing/mixins/bc-datetime/index.html | 1426 ++++ testing/mixins/bc-fake/index.html | 1054 +++ testing/mixins/bc-format/index.html | 2914 +++++++ testing/mixins/bc-random/index.html | 1500 ++++ testing/mixins/bc-request/index.html | 1583 ++++ testing/mixins/bc/index.html | 1567 ++++ testing/mocks/mock-requests/index.html | 1944 +++++ testing/mocks/using-mocks/index.html | 1229 +++ testing/runing-tests/index.html | 1173 +++ 77 files changed, 47327 insertions(+) create mode 100644 .nojekyll create mode 100644 404.html create mode 100644 apps/activities/index.html create mode 100644 apps/admissions/index.html create mode 100644 apps/monitoring/introduction/index.html create mode 100644 apps/monitoring/scripts/index.html create mode 100644 assets/_mkdocstrings.css create mode 100644 assets/images/favicon.png create mode 100644 assets/javascripts/bundle.c2be25ad.min.js create mode 100644 assets/javascripts/bundle.c2be25ad.min.js.map create mode 100644 assets/javascripts/lunr/min/lunr.ar.min.js create mode 100644 assets/javascripts/lunr/min/lunr.da.min.js create mode 100644 assets/javascripts/lunr/min/lunr.de.min.js create mode 100644 assets/javascripts/lunr/min/lunr.du.min.js create mode 100644 assets/javascripts/lunr/min/lunr.es.min.js create mode 100644 assets/javascripts/lunr/min/lunr.fi.min.js create mode 100644 assets/javascripts/lunr/min/lunr.fr.min.js create mode 100644 assets/javascripts/lunr/min/lunr.hi.min.js create mode 100644 assets/javascripts/lunr/min/lunr.hu.min.js create mode 100644 assets/javascripts/lunr/min/lunr.hy.min.js create mode 100644 assets/javascripts/lunr/min/lunr.it.min.js create mode 100644 assets/javascripts/lunr/min/lunr.ja.min.js create mode 100644 assets/javascripts/lunr/min/lunr.jp.min.js create mode 100644 assets/javascripts/lunr/min/lunr.kn.min.js create mode 100644 assets/javascripts/lunr/min/lunr.ko.min.js create mode 100644 assets/javascripts/lunr/min/lunr.multi.min.js create mode 100644 assets/javascripts/lunr/min/lunr.nl.min.js create mode 100644 assets/javascripts/lunr/min/lunr.no.min.js create mode 100644 assets/javascripts/lunr/min/lunr.pt.min.js create mode 100644 assets/javascripts/lunr/min/lunr.ro.min.js create mode 100644 assets/javascripts/lunr/min/lunr.ru.min.js create mode 100644 assets/javascripts/lunr/min/lunr.sa.min.js create mode 100644 assets/javascripts/lunr/min/lunr.stemmer.support.min.js create mode 100644 assets/javascripts/lunr/min/lunr.sv.min.js create mode 100644 assets/javascripts/lunr/min/lunr.ta.min.js create mode 100644 assets/javascripts/lunr/min/lunr.te.min.js create mode 100644 assets/javascripts/lunr/min/lunr.th.min.js create mode 100644 assets/javascripts/lunr/min/lunr.tr.min.js create mode 100644 assets/javascripts/lunr/min/lunr.vi.min.js create mode 100644 assets/javascripts/lunr/min/lunr.zh.min.js create mode 100644 assets/javascripts/lunr/tinyseg.js create mode 100644 assets/javascripts/lunr/wordcut.js create mode 100644 assets/javascripts/workers/search.208ed371.min.js create mode 100644 assets/javascripts/workers/search.208ed371.min.js.map create mode 100644 assets/stylesheets/main.85bb2934.min.css create mode 100644 assets/stylesheets/main.85bb2934.min.css.map create mode 100644 assets/stylesheets/palette.a6bdf11c.min.css create mode 100644 assets/stylesheets/palette.a6bdf11c.min.css.map create mode 100644 deployment/configuring-the-github-secrets/index.html create mode 100644 deployment/environment-variables/index.html create mode 100644 endponts/index.html create mode 100644 images/dockerhub.PNG create mode 100644 images/github-secrets.PNG create mode 100644 index.html create mode 100644 installation/environment-variables/index.html create mode 100644 installation/fixtures/index.html create mode 100644 objects.inv create mode 100644 search/search_index.json create mode 100644 security/capabilities/index.html create mode 100644 services/google_cloud/google-cloud-functions/index.html create mode 100644 services/google_cloud/storage/index.html create mode 100644 services/slack integration/icons/index.html create mode 100644 signals/quickstart/index.html create mode 100644 sitemap.xml create mode 100644 sitemap.xml.gz create mode 100644 testing/mixins/bc-cache/index.html create mode 100644 testing/mixins/bc-check/index.html create mode 100644 testing/mixins/bc-database/index.html create mode 100644 testing/mixins/bc-datetime/index.html create mode 100644 testing/mixins/bc-fake/index.html create mode 100644 testing/mixins/bc-format/index.html create mode 100644 testing/mixins/bc-random/index.html create mode 100644 testing/mixins/bc-request/index.html create mode 100644 testing/mixins/bc/index.html create mode 100644 testing/mocks/mock-requests/index.html create mode 100644 testing/mocks/using-mocks/index.html create mode 100644 testing/runing-tests/index.html diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 000000000..e69de29bb diff --git a/404.html b/404.html new file mode 100644 index 000000000..684bcdcae --- /dev/null +++ b/404.html @@ -0,0 +1,1018 @@ + + + + + + + + + + + + + + + + + + BreatheCode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ +

404 - Not found

+ +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/apps/activities/index.html b/apps/activities/index.html new file mode 100644 index 000000000..87807c63a --- /dev/null +++ b/apps/activities/index.html @@ -0,0 +1,1166 @@ + + + + + + + + + + + + + + + + + + + + + + + + Activities - BreatheCode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + +

Activities

+ +

Activity API

+

This API uses Google DataStore as storage, there is not local storage on Heroku or Postgres.

+

We need Google DataStore because we plan to store huge amounts of activities that the user can do inside breathecode.

+

Possible activities (so far): +

"breathecode_login" //every time it logs in
+"online_platform_registration" //first day using breathecode
+"public_event_attendance" //attendy on an eventbrite event
+"classroom_attendance" //when the student attent to class
+"classroom_unattendance" //when the student miss class
+"lesson_opened" //when a lessons is opened on the platform
+"office_attendance" //when the office raspberry pi detects the student
+"nps_survey_answered" //when a nps survey is answered by the student
+"exercise_success" //when student successfully tests exercise
+

+

Any activity has the following inputs:

+
    'cohort',
+    'data',
+    'day',
+    'slug',
+    'user_agent',
+
+

Endpoints for the user

+

Get recent user activity +

GET: activity/user/{email_or_id}?slug=activity_slug
+

+

Add a new user activity (requires authentication) +

POST: activity/user/{email_or_id}
+{
+    'slug' => 'activity_slug',
+    'data' => 'any aditional data (string or json-encoded-string)'
+}
+
+💡 Node: You can pass the cohort in the data json object and it will be possible to filter on the activity graph like this:
+
+{
+    'slug' => 'activity_slug',
+    'data' => "{ \"cohort\": \"mdc-iii\" }" (json encoded string with the cohort id)
+}
+

+

Endpoints for the Cohort

+

Get recent user activity +

GET: activity/cohort/{slug_or_id}?slug=activity_slug
+
+Endpoints for the coding_error's +
Get recent user coding_errors
+GET: activity/coding_error/{email_or_id}?slug=activity_slug
+
+
Add a new coding_error (requires authentication)
+POST: activity/coding_error/
+
+{
+    "user_id" => "my@email.com",
+    "slug" => "webpack_error",
+    "data" => "optional additional information about the error",
+    "message" => "file not found",
+    "name" => "module-not-found,
+    "severity" => "900",
+    "details" => "stack trace for the error as string"
+}
+

+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/apps/admissions/index.html b/apps/admissions/index.html new file mode 100644 index 000000000..ce83603d2 --- /dev/null +++ b/apps/admissions/index.html @@ -0,0 +1,1076 @@ + + + + + + + + + + + + + + + + + + + + + + + + BreatheCode.Admissions - BreatheCode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + +

BreatheCode.Admissions

+

This module take care of the academic side of breathecode: Students, Cohorts, Course (aka: Certificate), Syllabus, etc. These are some of the things you can do with the breathecode.admissions API:

+
    +
  1. Manage Academies (BreatheCode let's you divide the academic operations into several academies normally based on territory, for example: 4Geeks Academy Miami vs 4Geeks Academy Madrid).
  2. +
  3. Manage Academy Staff: There are multiple roles surroing an academy, here you can invite users to one or many academies and assign them roles based on their responsabilities.
  4. +
  5. Manage Students (invite and delete students).
  6. +
  7. Manage Cohorts: Every new batch of students that starts in a classroom with a start and end date is called a "Cohort".
  8. +
+

TODO: finish this documentation.

+

Commands

+

Sync academies

+
python manage.py sync_admissions academies
+
+

Override previous academies +

python manage.py sync_admissions academies --override
+

+

Sync courses

+
python manage.py sync_admissions certificates
+
+

Sync cohorts

+
python manage.py sync_admissions cohorts
+
+

Sync students

+

python manage.py sync_admissions students --limit=3
+
+Limit: the number of students to sync

+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/apps/monitoring/introduction/index.html b/apps/monitoring/introduction/index.html new file mode 100644 index 000000000..0140f0d5e --- /dev/null +++ b/apps/monitoring/introduction/index.html @@ -0,0 +1,1113 @@ + + + + + + + + + + + + + + + + + + + + + + + + Intro to monitoring - BreatheCode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + +

Intro to monitoring

+

This app is ideal for running diagnostic and reminders on the breathecode platform.

+

Installation

+
    +
  • +

    Setup the monitor app job for once a day, this is the command: +

    $ python manage.py monitor apps
    +

    +
  • +
  • +

    Setup the monitor script job for once a day, this is the command: +

    $ python manage.py monitor script
    +

    +
  • +
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/apps/monitoring/scripts/index.html b/apps/monitoring/scripts/index.html new file mode 100644 index 000000000..2279a5e13 --- /dev/null +++ b/apps/monitoring/scripts/index.html @@ -0,0 +1,1243 @@ + + + + + + + + + + + + + + + + + + + + + + + + Monitoring Scripts - BreatheCode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + +

Monitoring Scripts

+

A monitoring script is something that you want to execute recurrently withing the breathecode API, for example:

+

scripts/alert_pending_leads.py is a small python script that checks if there is FormEntry Marketing module database that are pending processing.

+

You can create a monitoring script to remind academy staff members about things, or to remind students about pending homework, etc.

+

Stepts to create a new script:

+
    +
  1. create a new python file inside ./breathecode/monitoring/scripts
  2. +
  3. make sure your script starts with this content always:
  4. +
+
#!/usr/bin/env python
+"""
+Alert when there are Form Entries with status = PENDING
+"""
+from breathecode.utils import ScriptNotification
+# start your code here
+
+
    +
  1. You have access to the entire breathecode API from here, you can import models, services or any other class or variable from any file.
  2. +
  3. You can raise a ScriptNotification to notify for MINOR or CRITICAL reasons, for example:
  4. +
+

# here we are raising a notification because there are 2 pending tasks
+raise ScriptNotification("There are 2 pending taks", status='MINOR', slug="pending_tasks")
+
+5. If you don't raise any ScriptNotification and there are no other Exceptions in the script, it will be considered successfull and no notifications will trigger. +6. When a ScriptNotification has been raise the Application owner will receive a notification to the application.email and slack channel configured for notifications. +7. Check for other scripts as examples. +8. Test your script.

+

Global Context

+

There are some global variables that you have available during your scripts:

+ + + + + + + + + + + + + +
Variable nameValue
academyContains the academy model object, you can use it to retrieve the current academy id like this: query.filter(academy__id=academy.id)
+

Manually running your script

+

You can test your scripts by running the following command:

+
$ python manage.py run_script <file_name>
+
+# For example you can test the alert_pending_leads script like this:
+$ python manage.py run_script alert_pending_leads.py
+
+

Example Script

+

The following script checks for pending leads to process:

+
#!/usr/bin/env python
+"""
+Alert when there are Form Entries with status = PENDING
+"""
+from breathecode.marketing.models import FormEntry
+from django.db.models import Q
+from breathecode.utils import ScriptNotification
+
+# check the database for pending leads
+pending_leads = FormEntry.objects.filter(storage_status="PENDING").filter(Q(academy__id=academy.id) | Q(location=academy.slug))
+
+# trigger notification because pending leads were found
+if len(pending_leads) > 0:
+    raise ScriptNotification(f"Warning there are {len(pending_leads)} pending form entries", status='MINOR')
+
+# You can print this and it will show on the script results
+print("No pending leads")
+
+

Unit testing your script

+

from breathecode.monitoring.actions import run_script

+
script = run_script(model.monitor_script)
+
+del script['slack_payload']
+del script['title']
+
+expected = {'details': script['details'],
+            'severity_level': 5,
+            'status': script['status'],
+            'text': script['text']
+            }
+
+self.assertEqual(script, expected)
+
+self.assertEqual(self.all_monitor_script_dict(), [{
+    **self.model_to_dict(model, 'monitor_script'),
+}])
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/assets/_mkdocstrings.css b/assets/_mkdocstrings.css new file mode 100644 index 000000000..a65078d02 --- /dev/null +++ b/assets/_mkdocstrings.css @@ -0,0 +1,36 @@ + +/* Don't capitalize names. */ +h5.doc-heading { + text-transform: none !important; +} + +/* Avoid breaking parameters name, etc. in table cells. */ +.doc-contents td code { + word-break: normal !important; +} + +/* For pieces of Markdown rendered in table cells. */ +.doc-contents td p { + margin-top: 0 !important; + margin-bottom: 0 !important; +} + +/* Max width for docstring sections tables. */ +.doc .md-typeset__table, +.doc .md-typeset__table table { + display: table !important; + width: 100%; +} +.doc .md-typeset__table tr { + display: table-row; +} + +/* Avoid line breaks in rendered fields. */ +.field-body p { + display: inline; +} + +/* Defaults in Spacy table style. */ +.doc-param-default { + float: right; +} diff --git a/assets/images/favicon.png b/assets/images/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..1cf13b9f9d978896599290a74f77d5dbe7d1655c GIT binary patch literal 1870 zcmV-U2eJ5xP)Gc)JR9QMau)O=X#!i9;T z37kk-upj^(fsR36MHs_+1RCI)NNu9}lD0S{B^g8PN?Ww(5|~L#Ng*g{WsqleV}|#l zz8@ri&cTzw_h33bHI+12+kK6WN$h#n5cD8OQt`5kw6p~9H3()bUQ8OS4Q4HTQ=1Ol z_JAocz`fLbT2^{`8n~UAo=#AUOf=SOq4pYkt;XbC&f#7lb$*7=$na!mWCQ`dBQsO0 zLFBSPj*N?#u5&pf2t4XjEGH|=pPQ8xh7tpx;US5Cx_Ju;!O`ya-yF`)b%TEt5>eP1ZX~}sjjA%FJF?h7cX8=b!DZl<6%Cv z*G0uvvU+vmnpLZ2paivG-(cd*y3$hCIcsZcYOGh{$&)A6*XX&kXZd3G8m)G$Zz-LV z^GF3VAW^Mdv!)4OM8EgqRiz~*Cji;uzl2uC9^=8I84vNp;ltJ|q-*uQwGp2ma6cY7 z;`%`!9UXO@fr&Ebapfs34OmS9^u6$)bJxrucutf>`dKPKT%%*d3XlFVKunp9 zasduxjrjs>f8V=D|J=XNZp;_Zy^WgQ$9WDjgY=z@stwiEBm9u5*|34&1Na8BMjjgf3+SHcr`5~>oz1Y?SW^=K z^bTyO6>Gar#P_W2gEMwq)ot3; zREHn~U&Dp0l6YT0&k-wLwYjb?5zGK`W6S2v+K>AM(95m2C20L|3m~rN8dprPr@t)5lsk9Hu*W z?pS990s;Ez=+Rj{x7p``4>+c0G5^pYnB1^!TL=(?HLHZ+HicG{~4F1d^5Awl_2!1jICM-!9eoLhbbT^;yHcefyTAaqRcY zmuctDopPT!%k+}x%lZRKnzykr2}}XfG_ne?nRQO~?%hkzo;@RN{P6o`&mMUWBYMTe z6i8ChtjX&gXl`nvrU>jah)2iNM%JdjqoaeaU%yVn!^70x-flljp6Q5tK}5}&X8&&G zX3fpb3E(!rH=zVI_9Gjl45w@{(ITqngWFe7@9{mX;tO25Z_8 zQHEpI+FkTU#4xu>RkN>b3Tnc3UpWzPXWm#o55GKF09j^Mh~)K7{QqbO_~(@CVq! zS<8954|P8mXN2MRs86xZ&Q4EfM@JB94b=(YGuk)s&^jiSF=t3*oNK3`rD{H`yQ?d; ztE=laAUoZx5?RC8*WKOj`%LXEkgDd>&^Q4M^z`%u0rg-It=hLCVsq!Z%^6eB-OvOT zFZ28TN&cRmgU}Elrnk43)!>Z1FCPL2K$7}gwzIc48NX}#!A1BpJP?#v5wkNprhV** z?Cpalt1oH&{r!o3eSKc&ap)iz2BTn_VV`4>9M^b3;(YY}4>#ML6{~(4mH+?%07*qo IM6N<$f(jP3KmY&$ literal 0 HcmV?d00001 diff --git a/assets/javascripts/bundle.c2be25ad.min.js b/assets/javascripts/bundle.c2be25ad.min.js new file mode 100644 index 000000000..f32333ee1 --- /dev/null +++ b/assets/javascripts/bundle.c2be25ad.min.js @@ -0,0 +1,29 @@ +"use strict";(()=>{var Ci=Object.create;var gr=Object.defineProperty;var Ri=Object.getOwnPropertyDescriptor;var ki=Object.getOwnPropertyNames,Ht=Object.getOwnPropertySymbols,Hi=Object.getPrototypeOf,yr=Object.prototype.hasOwnProperty,nn=Object.prototype.propertyIsEnumerable;var rn=(e,t,r)=>t in e?gr(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r,P=(e,t)=>{for(var r in t||(t={}))yr.call(t,r)&&rn(e,r,t[r]);if(Ht)for(var r of Ht(t))nn.call(t,r)&&rn(e,r,t[r]);return e};var on=(e,t)=>{var r={};for(var n in e)yr.call(e,n)&&t.indexOf(n)<0&&(r[n]=e[n]);if(e!=null&&Ht)for(var n of Ht(e))t.indexOf(n)<0&&nn.call(e,n)&&(r[n]=e[n]);return r};var Pt=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var Pi=(e,t,r,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of ki(t))!yr.call(e,o)&&o!==r&&gr(e,o,{get:()=>t[o],enumerable:!(n=Ri(t,o))||n.enumerable});return e};var yt=(e,t,r)=>(r=e!=null?Ci(Hi(e)):{},Pi(t||!e||!e.__esModule?gr(r,"default",{value:e,enumerable:!0}):r,e));var sn=Pt((xr,an)=>{(function(e,t){typeof xr=="object"&&typeof an!="undefined"?t():typeof define=="function"&&define.amd?define(t):t()})(xr,function(){"use strict";function e(r){var n=!0,o=!1,i=null,s={text:!0,search:!0,url:!0,tel:!0,email:!0,password:!0,number:!0,date:!0,month:!0,week:!0,time:!0,datetime:!0,"datetime-local":!0};function a(O){return!!(O&&O!==document&&O.nodeName!=="HTML"&&O.nodeName!=="BODY"&&"classList"in O&&"contains"in O.classList)}function f(O){var Qe=O.type,De=O.tagName;return!!(De==="INPUT"&&s[Qe]&&!O.readOnly||De==="TEXTAREA"&&!O.readOnly||O.isContentEditable)}function c(O){O.classList.contains("focus-visible")||(O.classList.add("focus-visible"),O.setAttribute("data-focus-visible-added",""))}function u(O){O.hasAttribute("data-focus-visible-added")&&(O.classList.remove("focus-visible"),O.removeAttribute("data-focus-visible-added"))}function p(O){O.metaKey||O.altKey||O.ctrlKey||(a(r.activeElement)&&c(r.activeElement),n=!0)}function m(O){n=!1}function d(O){a(O.target)&&(n||f(O.target))&&c(O.target)}function h(O){a(O.target)&&(O.target.classList.contains("focus-visible")||O.target.hasAttribute("data-focus-visible-added"))&&(o=!0,window.clearTimeout(i),i=window.setTimeout(function(){o=!1},100),u(O.target))}function v(O){document.visibilityState==="hidden"&&(o&&(n=!0),Y())}function Y(){document.addEventListener("mousemove",N),document.addEventListener("mousedown",N),document.addEventListener("mouseup",N),document.addEventListener("pointermove",N),document.addEventListener("pointerdown",N),document.addEventListener("pointerup",N),document.addEventListener("touchmove",N),document.addEventListener("touchstart",N),document.addEventListener("touchend",N)}function B(){document.removeEventListener("mousemove",N),document.removeEventListener("mousedown",N),document.removeEventListener("mouseup",N),document.removeEventListener("pointermove",N),document.removeEventListener("pointerdown",N),document.removeEventListener("pointerup",N),document.removeEventListener("touchmove",N),document.removeEventListener("touchstart",N),document.removeEventListener("touchend",N)}function N(O){O.target.nodeName&&O.target.nodeName.toLowerCase()==="html"||(n=!1,B())}document.addEventListener("keydown",p,!0),document.addEventListener("mousedown",m,!0),document.addEventListener("pointerdown",m,!0),document.addEventListener("touchstart",m,!0),document.addEventListener("visibilitychange",v,!0),Y(),r.addEventListener("focus",d,!0),r.addEventListener("blur",h,!0),r.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&r.host?r.host.setAttribute("data-js-focus-visible",""):r.nodeType===Node.DOCUMENT_NODE&&(document.documentElement.classList.add("js-focus-visible"),document.documentElement.setAttribute("data-js-focus-visible",""))}if(typeof window!="undefined"&&typeof document!="undefined"){window.applyFocusVisiblePolyfill=e;var t;try{t=new CustomEvent("focus-visible-polyfill-ready")}catch(r){t=document.createEvent("CustomEvent"),t.initCustomEvent("focus-visible-polyfill-ready",!1,!1,{})}window.dispatchEvent(t)}typeof document!="undefined"&&e(document)})});var cn=Pt(Er=>{(function(e){var t=function(){try{return!!Symbol.iterator}catch(c){return!1}},r=t(),n=function(c){var u={next:function(){var p=c.shift();return{done:p===void 0,value:p}}};return r&&(u[Symbol.iterator]=function(){return u}),u},o=function(c){return encodeURIComponent(c).replace(/%20/g,"+")},i=function(c){return decodeURIComponent(String(c).replace(/\+/g," "))},s=function(){var c=function(p){Object.defineProperty(this,"_entries",{writable:!0,value:{}});var m=typeof p;if(m!=="undefined")if(m==="string")p!==""&&this._fromString(p);else if(p instanceof c){var d=this;p.forEach(function(B,N){d.append(N,B)})}else if(p!==null&&m==="object")if(Object.prototype.toString.call(p)==="[object Array]")for(var h=0;hd[0]?1:0}),c._entries&&(c._entries={});for(var p=0;p1?i(d[1]):"")}})})(typeof global!="undefined"?global:typeof window!="undefined"?window:typeof self!="undefined"?self:Er);(function(e){var t=function(){try{var o=new e.URL("b","http://a");return o.pathname="c d",o.href==="http://a/c%20d"&&o.searchParams}catch(i){return!1}},r=function(){var o=e.URL,i=function(f,c){typeof f!="string"&&(f=String(f)),c&&typeof c!="string"&&(c=String(c));var u=document,p;if(c&&(e.location===void 0||c!==e.location.href)){c=c.toLowerCase(),u=document.implementation.createHTMLDocument(""),p=u.createElement("base"),p.href=c,u.head.appendChild(p);try{if(p.href.indexOf(c)!==0)throw new Error(p.href)}catch(O){throw new Error("URL unable to set base "+c+" due to "+O)}}var m=u.createElement("a");m.href=f,p&&(u.body.appendChild(m),m.href=m.href);var d=u.createElement("input");if(d.type="url",d.value=f,m.protocol===":"||!/:/.test(m.href)||!d.checkValidity()&&!c)throw new TypeError("Invalid URL");Object.defineProperty(this,"_anchorElement",{value:m});var h=new e.URLSearchParams(this.search),v=!0,Y=!0,B=this;["append","delete","set"].forEach(function(O){var Qe=h[O];h[O]=function(){Qe.apply(h,arguments),v&&(Y=!1,B.search=h.toString(),Y=!0)}}),Object.defineProperty(this,"searchParams",{value:h,enumerable:!0});var N=void 0;Object.defineProperty(this,"_updateSearchParams",{enumerable:!1,configurable:!1,writable:!1,value:function(){this.search!==N&&(N=this.search,Y&&(v=!1,this.searchParams._fromString(this.search),v=!0))}})},s=i.prototype,a=function(f){Object.defineProperty(s,f,{get:function(){return this._anchorElement[f]},set:function(c){this._anchorElement[f]=c},enumerable:!0})};["hash","host","hostname","port","protocol"].forEach(function(f){a(f)}),Object.defineProperty(s,"search",{get:function(){return this._anchorElement.search},set:function(f){this._anchorElement.search=f,this._updateSearchParams()},enumerable:!0}),Object.defineProperties(s,{toString:{get:function(){var f=this;return function(){return f.href}}},href:{get:function(){return this._anchorElement.href.replace(/\?$/,"")},set:function(f){this._anchorElement.href=f,this._updateSearchParams()},enumerable:!0},pathname:{get:function(){return this._anchorElement.pathname.replace(/(^\/?)/,"/")},set:function(f){this._anchorElement.pathname=f},enumerable:!0},origin:{get:function(){var f={"http:":80,"https:":443,"ftp:":21}[this._anchorElement.protocol],c=this._anchorElement.port!=f&&this._anchorElement.port!=="";return this._anchorElement.protocol+"//"+this._anchorElement.hostname+(c?":"+this._anchorElement.port:"")},enumerable:!0},password:{get:function(){return""},set:function(f){},enumerable:!0},username:{get:function(){return""},set:function(f){},enumerable:!0}}),i.createObjectURL=function(f){return o.createObjectURL.apply(o,arguments)},i.revokeObjectURL=function(f){return o.revokeObjectURL.apply(o,arguments)},e.URL=i};if(t()||r(),e.location!==void 0&&!("origin"in e.location)){var n=function(){return e.location.protocol+"//"+e.location.hostname+(e.location.port?":"+e.location.port:"")};try{Object.defineProperty(e.location,"origin",{get:n,enumerable:!0})}catch(o){setInterval(function(){e.location.origin=n()},100)}}})(typeof global!="undefined"?global:typeof window!="undefined"?window:typeof self!="undefined"?self:Er)});var qr=Pt((Mt,Nr)=>{/*! + * clipboard.js v2.0.11 + * https://clipboardjs.com/ + * + * Licensed MIT © Zeno Rocha + */(function(t,r){typeof Mt=="object"&&typeof Nr=="object"?Nr.exports=r():typeof define=="function"&&define.amd?define([],r):typeof Mt=="object"?Mt.ClipboardJS=r():t.ClipboardJS=r()})(Mt,function(){return function(){var e={686:function(n,o,i){"use strict";i.d(o,{default:function(){return Ai}});var s=i(279),a=i.n(s),f=i(370),c=i.n(f),u=i(817),p=i.n(u);function m(j){try{return document.execCommand(j)}catch(T){return!1}}var d=function(T){var E=p()(T);return m("cut"),E},h=d;function v(j){var T=document.documentElement.getAttribute("dir")==="rtl",E=document.createElement("textarea");E.style.fontSize="12pt",E.style.border="0",E.style.padding="0",E.style.margin="0",E.style.position="absolute",E.style[T?"right":"left"]="-9999px";var H=window.pageYOffset||document.documentElement.scrollTop;return E.style.top="".concat(H,"px"),E.setAttribute("readonly",""),E.value=j,E}var Y=function(T,E){var H=v(T);E.container.appendChild(H);var I=p()(H);return m("copy"),H.remove(),I},B=function(T){var E=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body},H="";return typeof T=="string"?H=Y(T,E):T instanceof HTMLInputElement&&!["text","search","url","tel","password"].includes(T==null?void 0:T.type)?H=Y(T.value,E):(H=p()(T),m("copy")),H},N=B;function O(j){return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?O=function(E){return typeof E}:O=function(E){return E&&typeof Symbol=="function"&&E.constructor===Symbol&&E!==Symbol.prototype?"symbol":typeof E},O(j)}var Qe=function(){var T=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},E=T.action,H=E===void 0?"copy":E,I=T.container,q=T.target,Me=T.text;if(H!=="copy"&&H!=="cut")throw new Error('Invalid "action" value, use either "copy" or "cut"');if(q!==void 0)if(q&&O(q)==="object"&&q.nodeType===1){if(H==="copy"&&q.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if(H==="cut"&&(q.hasAttribute("readonly")||q.hasAttribute("disabled")))throw new Error(`Invalid "target" attribute. You can't cut text from elements with "readonly" or "disabled" attributes`)}else throw new Error('Invalid "target" value, use a valid Element');if(Me)return N(Me,{container:I});if(q)return H==="cut"?h(q):N(q,{container:I})},De=Qe;function $e(j){return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?$e=function(E){return typeof E}:$e=function(E){return E&&typeof Symbol=="function"&&E.constructor===Symbol&&E!==Symbol.prototype?"symbol":typeof E},$e(j)}function Ei(j,T){if(!(j instanceof T))throw new TypeError("Cannot call a class as a function")}function tn(j,T){for(var E=0;E0&&arguments[0]!==void 0?arguments[0]:{};this.action=typeof I.action=="function"?I.action:this.defaultAction,this.target=typeof I.target=="function"?I.target:this.defaultTarget,this.text=typeof I.text=="function"?I.text:this.defaultText,this.container=$e(I.container)==="object"?I.container:document.body}},{key:"listenClick",value:function(I){var q=this;this.listener=c()(I,"click",function(Me){return q.onClick(Me)})}},{key:"onClick",value:function(I){var q=I.delegateTarget||I.currentTarget,Me=this.action(q)||"copy",kt=De({action:Me,container:this.container,target:this.target(q),text:this.text(q)});this.emit(kt?"success":"error",{action:Me,text:kt,trigger:q,clearSelection:function(){q&&q.focus(),window.getSelection().removeAllRanges()}})}},{key:"defaultAction",value:function(I){return vr("action",I)}},{key:"defaultTarget",value:function(I){var q=vr("target",I);if(q)return document.querySelector(q)}},{key:"defaultText",value:function(I){return vr("text",I)}},{key:"destroy",value:function(){this.listener.destroy()}}],[{key:"copy",value:function(I){var q=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body};return N(I,q)}},{key:"cut",value:function(I){return h(I)}},{key:"isSupported",value:function(){var I=arguments.length>0&&arguments[0]!==void 0?arguments[0]:["copy","cut"],q=typeof I=="string"?[I]:I,Me=!!document.queryCommandSupported;return q.forEach(function(kt){Me=Me&&!!document.queryCommandSupported(kt)}),Me}}]),E}(a()),Ai=Li},828:function(n){var o=9;if(typeof Element!="undefined"&&!Element.prototype.matches){var i=Element.prototype;i.matches=i.matchesSelector||i.mozMatchesSelector||i.msMatchesSelector||i.oMatchesSelector||i.webkitMatchesSelector}function s(a,f){for(;a&&a.nodeType!==o;){if(typeof a.matches=="function"&&a.matches(f))return a;a=a.parentNode}}n.exports=s},438:function(n,o,i){var s=i(828);function a(u,p,m,d,h){var v=c.apply(this,arguments);return u.addEventListener(m,v,h),{destroy:function(){u.removeEventListener(m,v,h)}}}function f(u,p,m,d,h){return typeof u.addEventListener=="function"?a.apply(null,arguments):typeof m=="function"?a.bind(null,document).apply(null,arguments):(typeof u=="string"&&(u=document.querySelectorAll(u)),Array.prototype.map.call(u,function(v){return a(v,p,m,d,h)}))}function c(u,p,m,d){return function(h){h.delegateTarget=s(h.target,p),h.delegateTarget&&d.call(u,h)}}n.exports=f},879:function(n,o){o.node=function(i){return i!==void 0&&i instanceof HTMLElement&&i.nodeType===1},o.nodeList=function(i){var s=Object.prototype.toString.call(i);return i!==void 0&&(s==="[object NodeList]"||s==="[object HTMLCollection]")&&"length"in i&&(i.length===0||o.node(i[0]))},o.string=function(i){return typeof i=="string"||i instanceof String},o.fn=function(i){var s=Object.prototype.toString.call(i);return s==="[object Function]"}},370:function(n,o,i){var s=i(879),a=i(438);function f(m,d,h){if(!m&&!d&&!h)throw new Error("Missing required arguments");if(!s.string(d))throw new TypeError("Second argument must be a String");if(!s.fn(h))throw new TypeError("Third argument must be a Function");if(s.node(m))return c(m,d,h);if(s.nodeList(m))return u(m,d,h);if(s.string(m))return p(m,d,h);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList")}function c(m,d,h){return m.addEventListener(d,h),{destroy:function(){m.removeEventListener(d,h)}}}function u(m,d,h){return Array.prototype.forEach.call(m,function(v){v.addEventListener(d,h)}),{destroy:function(){Array.prototype.forEach.call(m,function(v){v.removeEventListener(d,h)})}}}function p(m,d,h){return a(document.body,m,d,h)}n.exports=f},817:function(n){function o(i){var s;if(i.nodeName==="SELECT")i.focus(),s=i.value;else if(i.nodeName==="INPUT"||i.nodeName==="TEXTAREA"){var a=i.hasAttribute("readonly");a||i.setAttribute("readonly",""),i.select(),i.setSelectionRange(0,i.value.length),a||i.removeAttribute("readonly"),s=i.value}else{i.hasAttribute("contenteditable")&&i.focus();var f=window.getSelection(),c=document.createRange();c.selectNodeContents(i),f.removeAllRanges(),f.addRange(c),s=f.toString()}return s}n.exports=o},279:function(n){function o(){}o.prototype={on:function(i,s,a){var f=this.e||(this.e={});return(f[i]||(f[i]=[])).push({fn:s,ctx:a}),this},once:function(i,s,a){var f=this;function c(){f.off(i,c),s.apply(a,arguments)}return c._=s,this.on(i,c,a)},emit:function(i){var s=[].slice.call(arguments,1),a=((this.e||(this.e={}))[i]||[]).slice(),f=0,c=a.length;for(f;f{"use strict";/*! + * escape-html + * Copyright(c) 2012-2013 TJ Holowaychuk + * Copyright(c) 2015 Andreas Lubbe + * Copyright(c) 2015 Tiancheng "Timothy" Gu + * MIT Licensed + */var rs=/["'&<>]/;Yo.exports=ns;function ns(e){var t=""+e,r=rs.exec(t);if(!r)return t;var n,o="",i=0,s=0;for(i=r.index;i0&&i[i.length-1])&&(c[0]===6||c[0]===2)){r=0;continue}if(c[0]===3&&(!i||c[1]>i[0]&&c[1]=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function W(e,t){var r=typeof Symbol=="function"&&e[Symbol.iterator];if(!r)return e;var n=r.call(e),o,i=[],s;try{for(;(t===void 0||t-- >0)&&!(o=n.next()).done;)i.push(o.value)}catch(a){s={error:a}}finally{try{o&&!o.done&&(r=n.return)&&r.call(n)}finally{if(s)throw s.error}}return i}function D(e,t,r){if(r||arguments.length===2)for(var n=0,o=t.length,i;n1||a(m,d)})})}function a(m,d){try{f(n[m](d))}catch(h){p(i[0][3],h)}}function f(m){m.value instanceof et?Promise.resolve(m.value.v).then(c,u):p(i[0][2],m)}function c(m){a("next",m)}function u(m){a("throw",m)}function p(m,d){m(d),i.shift(),i.length&&a(i[0][0],i[0][1])}}function pn(e){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var t=e[Symbol.asyncIterator],r;return t?t.call(e):(e=typeof Ee=="function"?Ee(e):e[Symbol.iterator](),r={},n("next"),n("throw"),n("return"),r[Symbol.asyncIterator]=function(){return this},r);function n(i){r[i]=e[i]&&function(s){return new Promise(function(a,f){s=e[i](s),o(a,f,s.done,s.value)})}}function o(i,s,a,f){Promise.resolve(f).then(function(c){i({value:c,done:a})},s)}}function C(e){return typeof e=="function"}function at(e){var t=function(n){Error.call(n),n.stack=new Error().stack},r=e(t);return r.prototype=Object.create(Error.prototype),r.prototype.constructor=r,r}var It=at(function(e){return function(r){e(this),this.message=r?r.length+` errors occurred during unsubscription: +`+r.map(function(n,o){return o+1+") "+n.toString()}).join(` + `):"",this.name="UnsubscriptionError",this.errors=r}});function Ve(e,t){if(e){var r=e.indexOf(t);0<=r&&e.splice(r,1)}}var Ie=function(){function e(t){this.initialTeardown=t,this.closed=!1,this._parentage=null,this._finalizers=null}return e.prototype.unsubscribe=function(){var t,r,n,o,i;if(!this.closed){this.closed=!0;var s=this._parentage;if(s)if(this._parentage=null,Array.isArray(s))try{for(var a=Ee(s),f=a.next();!f.done;f=a.next()){var c=f.value;c.remove(this)}}catch(v){t={error:v}}finally{try{f&&!f.done&&(r=a.return)&&r.call(a)}finally{if(t)throw t.error}}else s.remove(this);var u=this.initialTeardown;if(C(u))try{u()}catch(v){i=v instanceof It?v.errors:[v]}var p=this._finalizers;if(p){this._finalizers=null;try{for(var m=Ee(p),d=m.next();!d.done;d=m.next()){var h=d.value;try{ln(h)}catch(v){i=i!=null?i:[],v instanceof It?i=D(D([],W(i)),W(v.errors)):i.push(v)}}}catch(v){n={error:v}}finally{try{d&&!d.done&&(o=m.return)&&o.call(m)}finally{if(n)throw n.error}}}if(i)throw new It(i)}},e.prototype.add=function(t){var r;if(t&&t!==this)if(this.closed)ln(t);else{if(t instanceof e){if(t.closed||t._hasParent(this))return;t._addParent(this)}(this._finalizers=(r=this._finalizers)!==null&&r!==void 0?r:[]).push(t)}},e.prototype._hasParent=function(t){var r=this._parentage;return r===t||Array.isArray(r)&&r.includes(t)},e.prototype._addParent=function(t){var r=this._parentage;this._parentage=Array.isArray(r)?(r.push(t),r):r?[r,t]:t},e.prototype._removeParent=function(t){var r=this._parentage;r===t?this._parentage=null:Array.isArray(r)&&Ve(r,t)},e.prototype.remove=function(t){var r=this._finalizers;r&&Ve(r,t),t instanceof e&&t._removeParent(this)},e.EMPTY=function(){var t=new e;return t.closed=!0,t}(),e}();var Sr=Ie.EMPTY;function jt(e){return e instanceof Ie||e&&"closed"in e&&C(e.remove)&&C(e.add)&&C(e.unsubscribe)}function ln(e){C(e)?e():e.unsubscribe()}var Le={onUnhandledError:null,onStoppedNotification:null,Promise:void 0,useDeprecatedSynchronousErrorHandling:!1,useDeprecatedNextContext:!1};var st={setTimeout:function(e,t){for(var r=[],n=2;n0},enumerable:!1,configurable:!0}),t.prototype._trySubscribe=function(r){return this._throwIfClosed(),e.prototype._trySubscribe.call(this,r)},t.prototype._subscribe=function(r){return this._throwIfClosed(),this._checkFinalizedStatuses(r),this._innerSubscribe(r)},t.prototype._innerSubscribe=function(r){var n=this,o=this,i=o.hasError,s=o.isStopped,a=o.observers;return i||s?Sr:(this.currentObservers=null,a.push(r),new Ie(function(){n.currentObservers=null,Ve(a,r)}))},t.prototype._checkFinalizedStatuses=function(r){var n=this,o=n.hasError,i=n.thrownError,s=n.isStopped;o?r.error(i):s&&r.complete()},t.prototype.asObservable=function(){var r=new F;return r.source=this,r},t.create=function(r,n){return new xn(r,n)},t}(F);var xn=function(e){ie(t,e);function t(r,n){var o=e.call(this)||this;return o.destination=r,o.source=n,o}return t.prototype.next=function(r){var n,o;(o=(n=this.destination)===null||n===void 0?void 0:n.next)===null||o===void 0||o.call(n,r)},t.prototype.error=function(r){var n,o;(o=(n=this.destination)===null||n===void 0?void 0:n.error)===null||o===void 0||o.call(n,r)},t.prototype.complete=function(){var r,n;(n=(r=this.destination)===null||r===void 0?void 0:r.complete)===null||n===void 0||n.call(r)},t.prototype._subscribe=function(r){var n,o;return(o=(n=this.source)===null||n===void 0?void 0:n.subscribe(r))!==null&&o!==void 0?o:Sr},t}(x);var Et={now:function(){return(Et.delegate||Date).now()},delegate:void 0};var wt=function(e){ie(t,e);function t(r,n,o){r===void 0&&(r=1/0),n===void 0&&(n=1/0),o===void 0&&(o=Et);var i=e.call(this)||this;return i._bufferSize=r,i._windowTime=n,i._timestampProvider=o,i._buffer=[],i._infiniteTimeWindow=!0,i._infiniteTimeWindow=n===1/0,i._bufferSize=Math.max(1,r),i._windowTime=Math.max(1,n),i}return t.prototype.next=function(r){var n=this,o=n.isStopped,i=n._buffer,s=n._infiniteTimeWindow,a=n._timestampProvider,f=n._windowTime;o||(i.push(r),!s&&i.push(a.now()+f)),this._trimBuffer(),e.prototype.next.call(this,r)},t.prototype._subscribe=function(r){this._throwIfClosed(),this._trimBuffer();for(var n=this._innerSubscribe(r),o=this,i=o._infiniteTimeWindow,s=o._buffer,a=s.slice(),f=0;f0?e.prototype.requestAsyncId.call(this,r,n,o):(r.actions.push(this),r._scheduled||(r._scheduled=ut.requestAnimationFrame(function(){return r.flush(void 0)})))},t.prototype.recycleAsyncId=function(r,n,o){var i;if(o===void 0&&(o=0),o!=null?o>0:this.delay>0)return e.prototype.recycleAsyncId.call(this,r,n,o);var s=r.actions;n!=null&&((i=s[s.length-1])===null||i===void 0?void 0:i.id)!==n&&(ut.cancelAnimationFrame(n),r._scheduled=void 0)},t}(Wt);var Sn=function(e){ie(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t.prototype.flush=function(r){this._active=!0;var n=this._scheduled;this._scheduled=void 0;var o=this.actions,i;r=r||o.shift();do if(i=r.execute(r.state,r.delay))break;while((r=o[0])&&r.id===n&&o.shift());if(this._active=!1,i){for(;(r=o[0])&&r.id===n&&o.shift();)r.unsubscribe();throw i}},t}(Dt);var Oe=new Sn(wn);var _=new F(function(e){return e.complete()});function Vt(e){return e&&C(e.schedule)}function Cr(e){return e[e.length-1]}function Ye(e){return C(Cr(e))?e.pop():void 0}function Te(e){return Vt(Cr(e))?e.pop():void 0}function zt(e,t){return typeof Cr(e)=="number"?e.pop():t}var pt=function(e){return e&&typeof e.length=="number"&&typeof e!="function"};function Nt(e){return C(e==null?void 0:e.then)}function qt(e){return C(e[ft])}function Kt(e){return Symbol.asyncIterator&&C(e==null?void 0:e[Symbol.asyncIterator])}function Qt(e){return new TypeError("You provided "+(e!==null&&typeof e=="object"?"an invalid object":"'"+e+"'")+" where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.")}function zi(){return typeof Symbol!="function"||!Symbol.iterator?"@@iterator":Symbol.iterator}var Yt=zi();function Gt(e){return C(e==null?void 0:e[Yt])}function Bt(e){return un(this,arguments,function(){var r,n,o,i;return $t(this,function(s){switch(s.label){case 0:r=e.getReader(),s.label=1;case 1:s.trys.push([1,,9,10]),s.label=2;case 2:return[4,et(r.read())];case 3:return n=s.sent(),o=n.value,i=n.done,i?[4,et(void 0)]:[3,5];case 4:return[2,s.sent()];case 5:return[4,et(o)];case 6:return[4,s.sent()];case 7:return s.sent(),[3,2];case 8:return[3,10];case 9:return r.releaseLock(),[7];case 10:return[2]}})})}function Jt(e){return C(e==null?void 0:e.getReader)}function U(e){if(e instanceof F)return e;if(e!=null){if(qt(e))return Ni(e);if(pt(e))return qi(e);if(Nt(e))return Ki(e);if(Kt(e))return On(e);if(Gt(e))return Qi(e);if(Jt(e))return Yi(e)}throw Qt(e)}function Ni(e){return new F(function(t){var r=e[ft]();if(C(r.subscribe))return r.subscribe(t);throw new TypeError("Provided object does not correctly implement Symbol.observable")})}function qi(e){return new F(function(t){for(var r=0;r=2;return function(n){return n.pipe(e?A(function(o,i){return e(o,i,n)}):de,ge(1),r?He(t):Dn(function(){return new Zt}))}}function Vn(){for(var e=[],t=0;t=2,!0))}function pe(e){e===void 0&&(e={});var t=e.connector,r=t===void 0?function(){return new x}:t,n=e.resetOnError,o=n===void 0?!0:n,i=e.resetOnComplete,s=i===void 0?!0:i,a=e.resetOnRefCountZero,f=a===void 0?!0:a;return function(c){var u,p,m,d=0,h=!1,v=!1,Y=function(){p==null||p.unsubscribe(),p=void 0},B=function(){Y(),u=m=void 0,h=v=!1},N=function(){var O=u;B(),O==null||O.unsubscribe()};return y(function(O,Qe){d++,!v&&!h&&Y();var De=m=m!=null?m:r();Qe.add(function(){d--,d===0&&!v&&!h&&(p=$r(N,f))}),De.subscribe(Qe),!u&&d>0&&(u=new rt({next:function($e){return De.next($e)},error:function($e){v=!0,Y(),p=$r(B,o,$e),De.error($e)},complete:function(){h=!0,Y(),p=$r(B,s),De.complete()}}),U(O).subscribe(u))})(c)}}function $r(e,t){for(var r=[],n=2;ne.next(document)),e}function K(e,t=document){return Array.from(t.querySelectorAll(e))}function z(e,t=document){let r=ce(e,t);if(typeof r=="undefined")throw new ReferenceError(`Missing element: expected "${e}" to be present`);return r}function ce(e,t=document){return t.querySelector(e)||void 0}function _e(){return document.activeElement instanceof HTMLElement&&document.activeElement||void 0}function tr(e){return L(b(document.body,"focusin"),b(document.body,"focusout")).pipe(ke(1),l(()=>{let t=_e();return typeof t!="undefined"?e.contains(t):!1}),V(e===_e()),J())}function Xe(e){return{x:e.offsetLeft,y:e.offsetTop}}function Kn(e){return L(b(window,"load"),b(window,"resize")).pipe(Ce(0,Oe),l(()=>Xe(e)),V(Xe(e)))}function rr(e){return{x:e.scrollLeft,y:e.scrollTop}}function dt(e){return L(b(e,"scroll"),b(window,"resize")).pipe(Ce(0,Oe),l(()=>rr(e)),V(rr(e)))}var Yn=function(){if(typeof Map!="undefined")return Map;function e(t,r){var n=-1;return t.some(function(o,i){return o[0]===r?(n=i,!0):!1}),n}return function(){function t(){this.__entries__=[]}return Object.defineProperty(t.prototype,"size",{get:function(){return this.__entries__.length},enumerable:!0,configurable:!0}),t.prototype.get=function(r){var n=e(this.__entries__,r),o=this.__entries__[n];return o&&o[1]},t.prototype.set=function(r,n){var o=e(this.__entries__,r);~o?this.__entries__[o][1]=n:this.__entries__.push([r,n])},t.prototype.delete=function(r){var n=this.__entries__,o=e(n,r);~o&&n.splice(o,1)},t.prototype.has=function(r){return!!~e(this.__entries__,r)},t.prototype.clear=function(){this.__entries__.splice(0)},t.prototype.forEach=function(r,n){n===void 0&&(n=null);for(var o=0,i=this.__entries__;o0},e.prototype.connect_=function(){!Wr||this.connected_||(document.addEventListener("transitionend",this.onTransitionEnd_),window.addEventListener("resize",this.refresh),va?(this.mutationsObserver_=new MutationObserver(this.refresh),this.mutationsObserver_.observe(document,{attributes:!0,childList:!0,characterData:!0,subtree:!0})):(document.addEventListener("DOMSubtreeModified",this.refresh),this.mutationEventsAdded_=!0),this.connected_=!0)},e.prototype.disconnect_=function(){!Wr||!this.connected_||(document.removeEventListener("transitionend",this.onTransitionEnd_),window.removeEventListener("resize",this.refresh),this.mutationsObserver_&&this.mutationsObserver_.disconnect(),this.mutationEventsAdded_&&document.removeEventListener("DOMSubtreeModified",this.refresh),this.mutationsObserver_=null,this.mutationEventsAdded_=!1,this.connected_=!1)},e.prototype.onTransitionEnd_=function(t){var r=t.propertyName,n=r===void 0?"":r,o=ba.some(function(i){return!!~n.indexOf(i)});o&&this.refresh()},e.getInstance=function(){return this.instance_||(this.instance_=new e),this.instance_},e.instance_=null,e}(),Gn=function(e,t){for(var r=0,n=Object.keys(t);r0},e}(),Jn=typeof WeakMap!="undefined"?new WeakMap:new Yn,Xn=function(){function e(t){if(!(this instanceof e))throw new TypeError("Cannot call a class as a function.");if(!arguments.length)throw new TypeError("1 argument required, but only 0 present.");var r=ga.getInstance(),n=new La(t,r,this);Jn.set(this,n)}return e}();["observe","unobserve","disconnect"].forEach(function(e){Xn.prototype[e]=function(){var t;return(t=Jn.get(this))[e].apply(t,arguments)}});var Aa=function(){return typeof nr.ResizeObserver!="undefined"?nr.ResizeObserver:Xn}(),Zn=Aa;var eo=new x,Ca=$(()=>k(new Zn(e=>{for(let t of e)eo.next(t)}))).pipe(g(e=>L(ze,k(e)).pipe(R(()=>e.disconnect()))),X(1));function he(e){return{width:e.offsetWidth,height:e.offsetHeight}}function ye(e){return Ca.pipe(S(t=>t.observe(e)),g(t=>eo.pipe(A(({target:r})=>r===e),R(()=>t.unobserve(e)),l(()=>he(e)))),V(he(e)))}function bt(e){return{width:e.scrollWidth,height:e.scrollHeight}}function ar(e){let t=e.parentElement;for(;t&&(e.scrollWidth<=t.scrollWidth&&e.scrollHeight<=t.scrollHeight);)t=(e=t).parentElement;return t?e:void 0}var to=new x,Ra=$(()=>k(new IntersectionObserver(e=>{for(let t of e)to.next(t)},{threshold:0}))).pipe(g(e=>L(ze,k(e)).pipe(R(()=>e.disconnect()))),X(1));function sr(e){return Ra.pipe(S(t=>t.observe(e)),g(t=>to.pipe(A(({target:r})=>r===e),R(()=>t.unobserve(e)),l(({isIntersecting:r})=>r))))}function ro(e,t=16){return dt(e).pipe(l(({y:r})=>{let n=he(e),o=bt(e);return r>=o.height-n.height-t}),J())}var cr={drawer:z("[data-md-toggle=drawer]"),search:z("[data-md-toggle=search]")};function no(e){return cr[e].checked}function Ke(e,t){cr[e].checked!==t&&cr[e].click()}function Ue(e){let t=cr[e];return b(t,"change").pipe(l(()=>t.checked),V(t.checked))}function ka(e,t){switch(e.constructor){case HTMLInputElement:return e.type==="radio"?/^Arrow/.test(t):!0;case HTMLSelectElement:case HTMLTextAreaElement:return!0;default:return e.isContentEditable}}function Ha(){return L(b(window,"compositionstart").pipe(l(()=>!0)),b(window,"compositionend").pipe(l(()=>!1))).pipe(V(!1))}function oo(){let e=b(window,"keydown").pipe(A(t=>!(t.metaKey||t.ctrlKey)),l(t=>({mode:no("search")?"search":"global",type:t.key,claim(){t.preventDefault(),t.stopPropagation()}})),A(({mode:t,type:r})=>{if(t==="global"){let n=_e();if(typeof n!="undefined")return!ka(n,r)}return!0}),pe());return Ha().pipe(g(t=>t?_:e))}function le(){return new URL(location.href)}function ot(e){location.href=e.href}function io(){return new x}function ao(e,t){if(typeof t=="string"||typeof t=="number")e.innerHTML+=t.toString();else if(t instanceof Node)e.appendChild(t);else if(Array.isArray(t))for(let r of t)ao(e,r)}function M(e,t,...r){let n=document.createElement(e);if(t)for(let o of Object.keys(t))typeof t[o]!="undefined"&&(typeof t[o]!="boolean"?n.setAttribute(o,t[o]):n.setAttribute(o,""));for(let o of r)ao(n,o);return n}function fr(e){if(e>999){let t=+((e-950)%1e3>99);return`${((e+1e-6)/1e3).toFixed(t)}k`}else return e.toString()}function so(){return location.hash.substring(1)}function Dr(e){let t=M("a",{href:e});t.addEventListener("click",r=>r.stopPropagation()),t.click()}function Pa(e){return L(b(window,"hashchange"),e).pipe(l(so),V(so()),A(t=>t.length>0),X(1))}function co(e){return Pa(e).pipe(l(t=>ce(`[id="${t}"]`)),A(t=>typeof t!="undefined"))}function Vr(e){let t=matchMedia(e);return er(r=>t.addListener(()=>r(t.matches))).pipe(V(t.matches))}function fo(){let e=matchMedia("print");return L(b(window,"beforeprint").pipe(l(()=>!0)),b(window,"afterprint").pipe(l(()=>!1))).pipe(V(e.matches))}function zr(e,t){return e.pipe(g(r=>r?t():_))}function ur(e,t={credentials:"same-origin"}){return ue(fetch(`${e}`,t)).pipe(fe(()=>_),g(r=>r.status!==200?Ot(()=>new Error(r.statusText)):k(r)))}function We(e,t){return ur(e,t).pipe(g(r=>r.json()),X(1))}function uo(e,t){let r=new DOMParser;return ur(e,t).pipe(g(n=>n.text()),l(n=>r.parseFromString(n,"text/xml")),X(1))}function pr(e){let t=M("script",{src:e});return $(()=>(document.head.appendChild(t),L(b(t,"load"),b(t,"error").pipe(g(()=>Ot(()=>new ReferenceError(`Invalid script: ${e}`))))).pipe(l(()=>{}),R(()=>document.head.removeChild(t)),ge(1))))}function po(){return{x:Math.max(0,scrollX),y:Math.max(0,scrollY)}}function lo(){return L(b(window,"scroll",{passive:!0}),b(window,"resize",{passive:!0})).pipe(l(po),V(po()))}function mo(){return{width:innerWidth,height:innerHeight}}function ho(){return b(window,"resize",{passive:!0}).pipe(l(mo),V(mo()))}function bo(){return G([lo(),ho()]).pipe(l(([e,t])=>({offset:e,size:t})),X(1))}function lr(e,{viewport$:t,header$:r}){let n=t.pipe(ee("size")),o=G([n,r]).pipe(l(()=>Xe(e)));return G([r,t,o]).pipe(l(([{height:i},{offset:s,size:a},{x:f,y:c}])=>({offset:{x:s.x-f,y:s.y-c+i},size:a})))}(()=>{function e(n,o){parent.postMessage(n,o||"*")}function t(...n){return n.reduce((o,i)=>o.then(()=>new Promise(s=>{let a=document.createElement("script");a.src=i,a.onload=s,document.body.appendChild(a)})),Promise.resolve())}var r=class extends EventTarget{constructor(n){super(),this.url=n,this.m=i=>{i.source===this.w&&(this.dispatchEvent(new MessageEvent("message",{data:i.data})),this.onmessage&&this.onmessage(i))},this.e=(i,s,a,f,c)=>{if(s===`${this.url}`){let u=new ErrorEvent("error",{message:i,filename:s,lineno:a,colno:f,error:c});this.dispatchEvent(u),this.onerror&&this.onerror(u)}};let o=document.createElement("iframe");o.hidden=!0,document.body.appendChild(this.iframe=o),this.w.document.open(),this.w.document.write(` + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+ +
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/deployment/environment-variables/index.html b/deployment/environment-variables/index.html new file mode 100644 index 000000000..79d7fd6ff --- /dev/null +++ b/deployment/environment-variables/index.html @@ -0,0 +1,1233 @@ + + + + + + + + + + + + + + + + + + + + + + + + Environment variables - BreatheCode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + +

Environment variables

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
namedescription
ENVRepresents the current environment, can be DEVELOPMENT, TEST, and PRODUCTION
LOG_LEVELRepresents the log level for the logging module, can be NOTSET, DEBUG, INFO, WARNING, ERROR and CRITICAL
DATABASE_URLRepresents the connection string to the database, you can read more about schema url
CACHE_MIDDLEWARE_MINUTESRepresents how long an item will last in the cache
API_URLRepresents the url of api rest
ADMIN_URLRepresents the url of frontend of the admin
APP_URLRepresents the url of frontend of the webside
REDIS_URLRepresents the url of Redis
CELERY_TASK_SERIALIZERRepresents the default serialization method to use. Can be pickle json, yaml, msgpack or any custom serialization methods
EMAIL_NOTIFICATIONS_ENABLEDRepresents if the server can send notifications through email
SYSTEM_EMAILRepresents the email of Breathecode for support
GITHUB_CLIENT_IDRepresents the client id used for the OAuth2 with Github
GITHUB_SECRETRepresents the secret used for the OAuth2 with Github
GITHUB_REDIRECT_URLRepresents the redirect url used for the OAuth2 with Github
SLACK_CLIENT_IDRepresents the client id used for the OAuth2 with Slack
SLACK_SECRETRepresents the secret used for the OAuth2 with Slack
SLACK_REDIRECT_URLRepresents the redirect url used for the OAuth2 with Slack
MAILGUN_API_KEYRepresents the api key used for the OAuth2 with Mailgun
MAILGUN_DOMAINRepresents the domain of Breathecode that provided Mailgun
EVENTBRITE_KEYRepresents the key used for the OAuth2 with Eventbrite
FACEBOOK_VERIFY_TOKENRepresents the verify token used for the OAuth2 with Facebook
FACEBOOK_CLIENT_IDRepresents the client id used for the OAuth2 with Facebook
FACEBOOK_SECRETRepresents the secret used for the OAuth2 with Facebook
FACEBOOK_REDIRECT_URLRepresents the redirect url used for the OAuth2 with Facebook
ACTIVE_CAMPAIGN_KEYRepresents the key used for the OAuth2 with Active Campaign
ACTIVE_CAMPAIGN_URLRepresents the domain of Breathecode that provided Active Campaign
GOOGLE_APPLICATION_CREDENTIALSRepresents the file will be saved the service account of Google Cloud
GOOGLE_SERVICE_KEYRepresents the content of the service account used for the OAuth2 with Google Cloud
GOOGLE_PROJECT_IDProject ID on google cloud used for the integration of the entire API
GOOGLE_CLOUD_KEYRepresents the key used for the OAuth2 with Google Cloud
GOOGLE_CLIENT_IDRepresents the client id used for the OAuth2 with Google Cloud
GOOGLE_SECRETRepresents the secret used for the OAuth2 with Google Cloud
GOOGLE_REDIRECT_URLRepresents the redirect url used for the OAuth2 with Google Cloud
DAILY_API_KEYRepresents the api key used for the OAuth2 with Daily
DAILY_API_URLRepresents the domain of Breathecode that provided Daily
SAVE_LEADSRepresents if Breathecode will persist the leads
COMPANY_NAMERepresents the company name
COMPANY_CONTACT_URLRepresents the company contact url
COMPANY_LEGAL_NAMERepresents the company legal name
COMPANY_ADDRESSRepresents the company address
MEDIA_GALLERY_BUCKETRepresents the bucket for the media gallery
DOWNLOADS_BUCKETRepresents the bucket for the CSV files
PROFILE_BUCKETRepresents the bucket for profile avatars
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/endponts/index.html b/endponts/index.html new file mode 100644 index 000000000..73ac83807 --- /dev/null +++ b/endponts/index.html @@ -0,0 +1,1039 @@ + + + + + + + + + + + + + + + + + + + + Enpoints Documentation - BreatheCode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+ +
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/images/dockerhub.PNG b/images/dockerhub.PNG new file mode 100644 index 0000000000000000000000000000000000000000..a167404c43318259021414c5d38a346bf0551468 GIT binary patch literal 127183 zcmbTdc~nws+dqz(TB+HnIiK>Bm0FoOr!;DlTA351XyyoLrebP0@}u)U&pFTYuJ@1M`tG$@Y&Hw_eP83}`drt3dehQedWXUe z001C;^~$B&0Dwdo03f=!OgN=5XlZ zGe=nLA=z{1F5hf>W`FaN*zD!==Zfy^{z%*XG5Xve*=OnXI>s&)H+K9P{v>^J@!e>O zanQnu*8F@W$1*di)N;?E|2lp?-X6C5zm9}m|NpLN55Pxqqvpua0tMXM@w^bYO{wib zx%hwlyNY|UP4O%$$d^7r3tDGqpoW6@vG4p313up zb9ABPYAPH|E*zNjNx{PXQ@1uf>ZN|6AAeHu&+TH(->^rC(g4X0@m>C@vU81t#R*B8 z4zKrd^K##oR~VXXI|BF|S#h<}Hd}D?kAI}`{gumrd+7z8Ep6!^#A*`Nt@`Fj#W{C# zNKhr_B8p*KNBi+(*E_)4w$+uBjs4TDjZ%)u^3==Y#gu}*+ys$-{^*dq41)xnQNW)t zq3VWf?cLU~W9MhQmSz9oT)wyKl+FcQpzRSDt#dh7&&~)iGp z3~od{cWfoA?obW=P|0|zj+1uG+E4Z4gz_yo)vlBH8Y+_`ft)E@>2NSm963>myy7W3GY5Nm5F$r(b1jd*Aq$%GZv!t?nu9W6O{eXx2 z`X@s|NeYh0^*tO!9U{nxfuEb#;Z?gez+2|NRXb(|>mBPTr@6SDa^RBUaaugy~&GV+P1@<0_B*>c4+L#(V=*(N6>t8s#hFg7?u z(XU3j%S)k47f0LGBrl9d$E8MZioR6wB{EC{gYk6=`d$;>1h|9!a23+2^4>?A))w-| z_LEiD%2Qc)hiX^f^)Zn;AxgtN{;JG;%jqaghbCMVsoL;IfFpSbL^vym@bTJ@U+ASS zljR0E>on5o`^tYCwu%$His|2Dg5fQquXtnxI(epC{Kyp0eAVmRtF)4&pSX$7{yNtR zp?6A!_ZU?YsQ5Zs19Gz_lJv`Rc6A`L-zHg$yg#|KcQzlG6T0}i+r;BN_~+oxgqe{_ z`pu3cy*&o?-J*SYA)Akf>{7Y>8x$hB?E1GC^LXOOH>GD@*;)^zqI(g>8W*mtG{+d# z1m@?x{na5s+j|%F2iF={C-UIWt$138o&6dQ^P(a2<7g<`XyMj(AMJN;dzxh@8O%g_u;8AeXV=_hWnqrb2urti)QT@XVqH9P;d*dq<(LfRi^ zbgm?z*6M18_ftpa=5oV={8uz^>$!}o8kR2gfPeR9M^V`Ib2ICJ{4mS?=-2I^V>~F54g))<|a11tMjY zwD)7)Tj^r8N?aI;oKltU#oW8I(tHi@;@{!? z(He3*JcccaMuMvh4hE!k;^DNNMvM}FNBGtXCXFbL9c^UdjT6b>VXV>Z{)%vH=N~4u z;l8-as?cE!wa)Lk9*GAGa`;}gPE{mYhfd-({C*yS7X@eT@zClYQ|_*S|Dxi?J>cGt z{5&%x?iKH)79#8F0;k+oMsGDzoN&|zB+a6Ke{y$V12eCoc5eBT&wH(%=4t+PXtbu?>^{l^I&>iPD4}VQOw{c-mtkh;{jX@O^Z#yE!>u=v+Sm zyojIUeu~9sG`;FxHU@GOVb_N?KjdGV3$KAtI6-KyF!+M))Xa)Dk6yX5EW zaDSgqMX%0jBhH9(MqhZr+wFrBNz!lp^55(%u`=za#Jkc9elFqDZZWM&rQ?0xE1kY) z6#QOsJJB}|2JH~dd6kHxl@35JE@P?Sq&{brzeBe**=!i}#hWsM>uZvOJa)JZNKbVM zR4$M`#pB5sIX9gnXyuK@JNeRb_0!9o8W5iDFI7fgHRO*Jn4mXuMvDc5WDTRkVokp= z=%S&Og;#rmYI0CofOs_1mBY>y=pA2(_sI@aDe*`jv!Os%k`>o7{kV-Ojy(oUhf!2w zR4Z||$~scbgtpa3wSHI&fFJpiH9@YTIlTR~zJAAB9K(WgbQAzW-%vkRjoU@JFuq^I zb|!(i=_d*aTE$E{nYa2ZQX9+_T_>voNQ(=1`mUN;l$1sG0rRkd4)B0#CEFvOSeV&`@gkD<0_SsF zujoelW*yr~bYJ~t!0LHOU{Physu0R@8=o$Ki5NDsqc{X|fT^Hs@o@Q7eQDrA@EBZw zTd}<#vZjLy^bK#x)H;8QOB+1xhJ+4?mb==mmH#e(#Ns$SMulm}{52zNx z+~8^0(=N8p3wNi==bB4~5=^)id(^x7yRF)fHws?n0v=IbzrD7B3a|5V?Hu+2)K8Z7 z?B}65UW-Wv@P^H=y+$&Z6o*4uQ`bj`$q*xJeWyCIGK*142%|^_W2ytff*!OG0+!%wa=Dkx!KU z+XBRzZtEZDG^3B1=MuFh~mTHL$lcGhtQfBrd0} zdN%vno5+{9UvId`aYF=$&EOUT?>EAlv_5Ap%;>{uHm$8X-zU-`tL4~EJ4w%+IrarG zX7sE1ns|cxhdTI`*tC+e&YfX73BV|%+P~q#y}XqEH$Aee;^tKDc|Y4H#6~2y{`-Pj zHKY#S0dvH{Lb|@}>rL~{z?d-fswaF~s%NQn$QV6B{c*1yGhJO}y_rO^7p=9S{@8mS z@`FX-$$&^#os#az$~a3P^O$3hFe0(gaHm2}nY3<5lEj*?SP;~Ij% z9&Ro8u4`J4obdN-ZqrVue(Xxo(LYwjio6@NW9G}Yag)$`A!6O~2)wbgbkjcr<%GXL zEORKeDSjP8Ow7N(xvzH^u@#$=adEsv3T+_>FQ0waN|s>d!z`_aMf>K?w35Xvd~2#o zZdLlsq@h94QH|$Y@mEJnwuM=5^KLc!UfZ$3v>xha%WVgZI_A4>eNb^4pTLipM!GL- zbJoaO{*}8Qkgxk~&~Y(3$xGt3!)|~&rU$G_b<%%1O2F`Gdj=&dM|T-$9<^M)nE=@N z<|@wd9}7_%S)ZEad~jJU2X(0q|9WL&~T4x^z<7>B*Q{wuI)hUr~^zdGpVCx z8Yk0AIKC-K1}|UpXZaSYR|$qb_vpVkH~~6ZxJa{EZ{N{#p-!6l}Tu$>#w)m52EEF7imWdkqhF@O@==dns`ig?t#~J zXh3@X1`7T#e)e3MNctwgKyxrK$wXnBifHy!<0%7sR91>2!2Fx|MO*!VQ8@sio>hKs zv>7?fEV$Lx-N+WJX?E5>>&cdeMutj{JI5+HWX^NjB!oXvNu0jDBlB;(pnbjS6~4Jw z7bN)v?ig??J%##aK(C*Y7tU_=6#9&~H205yw5k$@lAXN}Ddg)L*ri!j8y)M#^%2wN zAV1gYEf{HwXGAcXC>u&kBK;5ct(O*9jG5r=4_<%Cuid;G;n_5+KjpEpF zn*yn{6@u-ANr+Dkj*_pu$pzh1YRDSAVYM^|brV*r1)J|buf9uByKP-7juj)~$9z24 zo2tmwr@7-~MUi>2T46byH~OlNh+3+Ha(vnwgf5BRaEl^eGLw>Mg$&F|(d-bq)?1u) zjH8-j4sEccqwBI{c&CR0df7j4ZkybacgCX_<9{jbvjyn4(xz5SAUmZ|7GOEN#oU7G zZK1ySJk##w``I6(-j}E9c6_**H3LtSz<<3~tA>bV@S6fvzf#>p3jOwsaSJAwXiCa= zB$hhz_5;-4UOhB5IZPlTSzBpY-hlL$qt-!hN~2QBzD5rIU?@e3Dez)%(b$P~Fw-BDmlOXZPhl@M3XAj&J&Gva9=!0K32Plm! zZt|Y*!2YelU$Nw@x#6n5FEVGBUP2$-fB3+AW|f-j)zK?N_ISA8YsDYjY@ZMu9QIck z^P7`EFc%QMS?C4l(V*l2?rcvrh1W2*tU5a7a^BZloMF{50~j(}<1*$E4;*d9nl{5e zuX8Yox6k}&5^jI^|~L^Zd6&~K-^^n6AyTV;pzAJ zF}Yo!<&KAhbbaqfkTqzaLxv0QTP$iVqp!)h^M+(t*UT=1lFsaPA1fVuOD&g|;y|0sK zZ~?XHSe&KhR|Va z=pblX$XD&u3{_x%TFOoTTyJukORAzZ2iodY!`){gMQ2Rh*;6+L`cGF)Y1q{TgA$TM zVv-DRLgq;A(!TINF{(2sz>ODxrE50jwC^~iM}(g5XdWx=9!|}VusOV|IO~we(j4h) zJ{n)!!hUrt5~+eWRde?Sz_qZuPq{Gtv-ZCe;Gyrkr*Rag#pos% z#*N>(i2O0vZHtRTb9n05E(2%_B&}loe$bywaDCrfLK>>J1Jk&n?igxg4^Y~Uj=Qlf zjH%EhI}h@?Pvwsrp=;h6vZ`9qDI~S?isi<^-`?cz8j=Y>JK^uH$ekpOs3v>$Rgge+ zsKoDk5m0^-98}v`CV+cV&MO zsbMk4Kb!4Hblh@UQ_`S+6!=5PX{T$5oN|Ar1aE}J7kb3a>vpP(pNwE2J#Mg1XZ$;XkPn=oNkIM;b%58!W{_m{|3 z4*tJg`B`Y4AO9Z|u5dNl(`osi!{RRdBX8kZTPqX>gGC<({2gxo`npp*TC7&$xX7u0 z^e#NRr*}%T*W~8^ZZuSeyM!-<{|7(w7nXV_EhQNsb7zb5f8P)0Z2vy={r}#SyJ+i{ zQPH=6|Ipa)6)PT)oe}5HyWIYo{_D-CAG+y_}sdEHWNWh8aC2!{jJJNZB^3$-hbOg&km0M>_#>? zpbu+*=~8DjHMFY1E~X*oFJ=GKgUXD|h}X(4a9wIQ=wRpvaQ{a>8{;==)XnyYnOuTnLiHQW9!VDDE+oWZGF!H zeTcD(U^kIqVV}Qq)YNtWx@et&XYRMgy!O7MkxP9mS%BzzSmb{Js@ph^z@bB4`X~!} zP&9DH4e7Ypclf_m{AioBr2MYk+B|u-O8jq4KgtdL_Ogp7M#%8+A=OoqXQ+y0y-(q( z1y1~H&z>I3s|~zfXm;ZusVE(y`)%5q^f1`w<71Rfq5V0VqJ6hoH5LY^R5!2?Bt8Kv zEOI{NtA4l4Fk*%MrPH%g(NR*dvG4d&|JwH1*UY8i&56Zcw_ns8M{4mU_}DT=<_qV#)_V`)K=-SL8={W}V-eTpc$T z?%l1wch+epeh~2ajnw}V<)7Y^DQWS_QFq^gJY~H(yRRd)T4d>P!QHxR9+{(N(h7FB zBhB{TsdPVPgR#9J+>u`-{r0DXTTc&}7s8&p+I+l=ve8PmB5C}vPCTMrdw=}Lect!f zrr22Cbs>HFE>7wK;Kea{+0*}N9KWutQn!N>w+!dI!w0zV?bMux_=lr4(1bmld6G}i zu+QQ7(Y%w5oAz2(B{_4rYr>}C_D^^GOF(zucF5hH{a{{rW$f(nM*%AP5tRFS%Bw`i;I<1b@P8IC&$;;cb%Mp1EJ$2i^S;na<~@c&|6;&~_e@E1E!N{-RJ9-e6w>^vYrxLyy$Oa|Zz^Q)@o0dwuz%O3vEd3pO zR1U!}`M!jj!{SDkg?0TlwJ`0&trx_FZiR~emshG>C97253PKT1#`0H_nb!tSOas$q zh9!}Em4Xa>f<|T!5Q6gkjh)i=QlQ;d1A{-SJ{N(BFT8CvO1`^L${JAUqS7^gb7{02g`Zz+wa@dzK+k(yDPuH8Q)AUfQKSHV-Q~XZ0|aDwaA0? zxQ)Ptb|V43Iy5m!uu|#1aptmB5c(!d@y+cVqfa&;@2lt*ytJ|!3CLdH6A`t!!4`aB zEn!rAet*X8y85{!jrR|uRtg?vpFRHl(N~`TYj)33=SywK{}GvvF(8vq;78wnHd>Rs z%OF{3N4f1BS>}UG{kp0jL`Q|1L+2eEX(}LLlTD}npYB)H`<|@~lR8W=rdEek$ zWd-l1NkZYkWo7{FSX_X`bE*-+M=XP$TT9h1DGGV}yztIM&Rd|bij5&7I+qUyw@`(V zL-Dt_oxMcWMe-TERHi94eZEnwrI}agC|W%nnl`dTX>S?FKAdIYY8*nlChFQh)$q1~ zgN|06obXk_*)V})O0}jrncDZC`^Y!gOrsE<5oBUcziz+|9QDVOXJ*SxRZ~>Ok$MR6+aME=L`^ zL|MzhA;X+A?re&J2}3e2jrXz9@yrE!r%I7igUZcf@xz0M^4>euPw$YPdz@eEgh(y( zx_Q+@7;JVOIFfnQQz8hYA)d6_D^23n%Ies0`NWGgMJ$|=e#X=V)|UB`AxgC0A# z4+hR5kobW7Mo`jwnRGZ)Leynym;Y-SD{7uq#x$-$BD%D_ME`eklF6_`?Jv&Sq)mAf zTE98}9G+ENG>@V4Gs{);^X}VHvK0AC?aClUf;m59rCUhW=@v8ESA#MBDjUTs)bio? zEo3$Kw(->-D=UGuKRTn-i+F=vcA@dG6A)R{1?(;s1*wiv(JHNehg3fUxNlgH%Y zG!7$M$ZpXwIsj8vVwVrZ9gAlH*l8q`4yPOt$!rV`y_djco%Q2dUCk1UENnwD~qJ!33yDKF+i?OT5=2@t|yn* zHXCMQ9~yqL``K29+=XAbx%OnG8XSwFR9E-1Bp6^xL;l`W%P*-;%)W^dfu_5WZt#o;C*vrEXy#8-bsTv?N5P$aj&^V?@xh#t;cH%g0*lZ zw{N>8bmZI(yKGqmKCN=T2uyFxu!zqOc zM3P%zo-6=pI2@{!^^;1zY6c{P8uOEf6QuKeey7rf0U2GbKPulpSV}Oy3$JpHxltVu z0};I12jC&yPSD21p0h=>u<)IPxUu0}2lU~e-+E_P4}#B3_FU4h2RHf=Oa;OD#U|10 zcgV$(ju+F95dAG*)FyCANuwDnN|M&_`;{gHAgH1h37ZyFb_h9a{B|7^{$)Tqi%Qg& z1<#!M4GzFMIV=39q)t<6);JhVh3wfSyH zlNH^#wMNX5ZpDJ_{t}-3^FgcBF!ej-owthY*!B&)0CSU|b@`B)+>p5;>}#K5higG+ z3f|MwYzIn98FT$dw2iVa>?UajRL#luVDd_->bOXT(1HKu#V!<%$vfy@k+K9OiD1?e z3t#o0w1?B6W^G4EZrrUM>mUam6PF5$i^{foIKU8fH|%=vARFEhG2q1G#b@p?HUi+0 z4n*qqK{v?fXz>==?^HRC?1C9ahnY&0lN$XH;>%P8(5{Z8bcZNj_NFqiMyKxuG`&m( z@(DD)Zpe_)fF&sI#nvWSe4eZU=;(+CqMr9DHf|RlyY{K+=q1T~*q=`_E~Tk%RJTf( z!7})`le%?=Zl>{XeqB6Q+}ffAcF&2I=;jFVMfu4Dcw2!Mfn^(Gp*XWRK%lKMuQE>) z*FH&he4xS5%tkE&^ETA(CNU1*K$oq9^D}QNd$E$^epJ2WbbN`0qWQ|MY%RjctY{+| zvEkH)-_^p!{=gASaB0RS8;?PCuh72@VJ7&lI3l~+bOx44)gDgP(%V97xyq0sSXgGT z*M=E`P6SyHC}Y!acyq(`7p{HOOhv-{0nHQ!acSREN>@kmA`@)v*WXk%mQR*9av0^) ztNz{oT|m>&%;97cvMp~*51ZH2hU~FQD;wi3xaBvn>sbUH=A&Ap9(fIqa3Y`P%v$G_ zw>M=<;Kx)>7Sm+H76UVlZ86LNx}1Bd%i=_Ab=+mZB0Oe^B$NP8DXlz>*Zer0ZbH{O%BC$s-AYdi;S320+h}i5>DXS(VdaqOsK=mLY8%r} z8Z1i|`P0`=Ij{04^aPCRX{v{gz+5746rXY#HfrEOY;LFex?)ip9(nAI<*aFhQZvPC z$&RBoqy9%xPsn_qXl>sFn^(fU(mG8ndL;=VCHsfW zF;qjKE#FZJ#*6zc?k=U8p#-nZEEfxZRXULij$!8V>bh-G`pt{4k`5_sg7ime`0@px z+ZK+NKJQPclSn#Dh1nFHR7UpBxIml4-_ysbCo|xqOxN!tiD2MOwy5R+mm;$W z150jU{;Jy5siGli2G4*)d*m3QV2?o16~5077teqz?x3{NxSkuVBnMu}3zLOr_y^kl z!7U~XImN@y9MymY@S1xdjsIruN-=T%k>(lvgF!OQpVT|>0ACgMJ2`jZ8+MKmMgPRZw`Cc<-Xq9MJw zqv{xb@mal-))@|uv;b~ke|iPnVme;~$3(tyl6<%Ry9oSl+Ipeuz3c;rFZZc%-pD{#KZYZ2}7!W zziImr-hpW_UE{^W_vG|3V83Gp0w|0Xz)ufkcpKwN#TnKPFByVf3U?&pcBqu*sfx|Y z_I@;SZ6cclO~`35E5Vdl8Uwc{l@n!4s2(R69=wfvJ!&*ONAqN7r73=vz?68?;Ncs@ zqoPfz>u*fD*^g+`2F2}jl4Re&T-NUp!mf4Nb!-v{+<*e}*-BL`heB33E{Lo8^y1eLlc=m5Pc2kbhVK9f$?+!2q7w?0O@4tK`w5RZpCc)Lzn4Sg^2QEwBWy% zQA3}L)ymFOk&0c=>8)(8Fsl#|^dwqv0&#?5N}wUwHKUn5_|Cb?_9m2)4?}3Wwumw4 zVmljzTM?$fERxnZ?xCH5aD%?I$=ymeFbt)y+y8Ofa)$iQu&%)i)gdhD#4H?ED;hHvN3v+qk^o^Gd+eNI8 z@-Rg?puc$;UzJ{HZE??n=*=TKs9y^vBsn{XDVWkf60|Euw`q9Pg)OmdjmDC?52)%0tA*11*E`z-14ZWnsQ&=R|C6A8_Y<51OSt98@ z;dZL2;^bV3RiqoNt%$@}R4;)0vS@P0HY>0<`}K|ftelRB^65KRgrk@tsJfBX-gY;O zd**Nzo2QLW5XZ1aJM)MiID1FT-yLI(hARb~ip81Y*y|l1ct)kJ%Y=k6-z9?&oZY3k z4sKI~A36mNRkMkhnwRXGadQVHkitFTRi+guA8ojarkfH@R+lC)QLDd(P!T!FrE#+T z)X*1jVJt==g)@7urpq%BAvOvtNk|Nj;sR0#$y!nknt5STg=uR z9^B7U`nY@xZ6nob2jN*VS@+d-sWOQ zI@!G%`=Hyb@Un!hi%2PsLiD8uhlyEq=q(^bUBK=eO(LoSsVpQfT!gx=lh&7UksNr4v`zp+8byvbMY;35yZ`HW15T;sCxS)f=H zbB$vidUA@=xAo2a(zwdgh-0O7I%0&OhVa8(Fj`(e9Jv&%R@<d?ikGTn1X_oy; zNC>wrI;0k^W-`_ou3}W0eWD0_LAhxE;P$EIxfe!LgL&Rh?+dBGe{h6}lD$D{|GsaH z!U<&KSKFcqddCN4?4-dzC2D0qh%<1^jb4)A^;a_Ql9eG`T^EIA_<9hS_rJ8Wcc({a z3FlQ#x6*@72?w}>dYu8Vfg;Dnbj|e~B>YYpvD@+oEK6QG48C<~I4h+6KCj$M7(1lE z9_qoHm{6U2zbPK}RI-cuD*JE*KVsNSc5SEv%iMWKvCtXfIdsL`cpdjD4%O>sy=2Ca zu#-IiZt|oJZ^!U0w3<#^OHRXThz-60N9E6is2+=oYkqm^BSH)pHrn{8ysrU4Z@g3f zDsywN-6b$LpSa-1B{e?4zU2=hd<@B7@Ui; zK!Xa-RpR4-lyNpnUNS@T@$$v^_nQdRa7n+f#{;@hBFSu7@%;R_zJoWhP6MXJAZ1y+ z!_3d*?d+#wGI|gD7Qin(a!4Dqp*ai=W9LGTFzbA28?Pd{FO^F{^W_^xOee(?1}K>J zObtDwk)&Z$_#H>k0L?v#{8>~wEV9Q?$6(8lnICA~`II-`2k4INrDRezfAO;4lRQMK zfl)O?&1wu5$x~Zo2Jezeo%fFgUd9nmhMt}GD`$~S1vNi?_mtV z5{_1AB2ft$tWvwF;q`ZmfQ7YNvA=qh9gA(3No-UITTx(f4>2pYRneX?sc zPYv(pC1#&j%4u!(%xTrr!S!kgoeDQh2G_ia=si`s)A$$XB1izL!JjCJui|#(q4C&} z#%)WHP0n@mC~u@|Ue%^^f^mp3PjQ}66@Tm_s~M4J6JxOm<151KK>S^$q>O&oz(34X ziSqpP7p5xcRa`^DP_l=tDNW|+#xk3Voy}l^zL4?x8Iu-;RuQ}%q4h1)7>B{^v*K zIzG*XF)(0OmjRkd%mfTAfE|BB#Qb#JW))oXWo#K8V_Z!X&OY*Yzr}Y`*b@>B3ulDW z>S+p;c$d#vUol_PY^XE#k~0xG1IWMXTLf_4(1SW<3fqS*twiD5$uh&RCwY#p!|`;} z>tG0vKN%BY3-QJzV0r9!Lj@38w~8y;xVSaR;S>~TS&72&hc;nE+s`U;g1{fXawURu zTgJy-RUgN51M^pvRtaB^Gaq+zC(%To`X!@EUc$OhSbK&oYV|^n! z`0cIel7SGdA?7$#o!cl+?wRe&Ok|5rVgn7Eg>e`-hW~>ib2$>U<~@}o>MZ$)sdNaY z2>iklj;$(=5E_oawqm0iWw(OQ!7v`^I!n5bzp5(_IwkDB=xMD;3x4x#O(o z4q4tmHyTq7_`%4t8>j|Ou(`C-!-QH6(eh+{NNFZ|{kX{}U$EM{7ak~3Q=e;$0Wp$j zX*1n*AjS*!l+8zb6_Q^Gmv}a0le)s{cBB;g^OENLx{rmh2a`f_1078sU%P1X0HotM zR+Va5>S#vV@#9v9!c@R4i=h7BorNRbs#KZ7vaz583S?P`q_s5q2kf0`AE9+M(ymzl zk#R|h?}Mr`t0BGFsdH?mL=*dRhiLaxi^QNrPO_s%dl}Si;2do`#B7VRBA=#r>fta2 zRqm@kD|?21=J=%gA(w*}LvT$bDNBLTy1ws|>Wnwy9qyNak5?kKBsE*VMMd56vT%Wd zkcAc^%2Kq>!3&rfq z4KltRvN5k+L5V3POja!Sxut9|3jLoHbHi$UX=2YJw?Hp2FDh$OR|_u8Ek0^WOocR5 zFxb60R!p~#r7E1#?F)Y{R^|gxW{GWPpXS&QaxRZU#YAxdV;d za2UvXmD_Pc!#C$7Kd>+RHT|daQ0O1l=OkK>>4en8!Eo(BgHL|fK+dy}MAF=Ljl>i# zr4To5bLs&b-Rmr@ZnUn?8hZ3Vp_q)st;uP9JrY6~(SbVZ$+`^m>Ek_Xq=(|=G|jB| zN{-zsI%&rZhO*ezxva_XRZEeKG3V1(&O`R61?AA>dYmza+@PiqAWiL0Hi(=#8-k{OYW9Iu(;gxTS=Zc2^rb4Iof z*xP`Em^0-TvsbB3=e~2lwhDf=-2U_+fvcugUV=bIr3c%nlLs zeXnbQxnu~(h+3c;y6K=yUPRm_%h_hZeMR1j)!-emPV_wtaf0WDx*Aaz{Gj+D*2`pRbLm?L#@`v&3$*wRjCw9 zWWZsor6v?OxOhM%X8folJyM7{=R6o-Xr;w==1Cdv0W=tBPh&QkMC0QGYr~W7$Sf+O zJySIgty)R`-9jG8SDn9PB{E76w!(*f-OQ+b|49bPKQ$z{3xzxZ8{DJOco}gKl{edD zo_&Z^D;~IT4!!!(Vmr(wH_DGa<`bB=uX>2+COb29<+kKc&HMasUNJa*`xC`4Ol84j z<2~ecO1wpJ9BRf@Z7*C=u%=~Dfi^CA0f`Z_{>{OpJ*2c;XQ%e-I4FR(R5aWl?5eJ$Y!CaGz5)Hi#RW=@2o?*9nwmD;TagQ0CGZe zBMwaw_SIBH?MH=k&ESF`n*~j?YISGhSp+Zsyu=IHogg&G6G}IPGhz@gX$A)*xH_2?S`EE%|6eCn^oe74g|u#|9ucV92NzG_jt<(s)gQyweDL~efM zh&@@qK{y?si=TdO(zuC&)twqBV%!Y*jMci@%n)mco>|>V8eDjDe6H8jOugzveqPu0 znOy5{#lamPjLHN3aK?pCoKOo!7K$Wuw3bp4@uzejHCPo&`Vq7`yb{ECDuCu>>Zccn z()4?EKsybzUO2(l@@b1N<1^qO}J z?%01YB!e>6GGclkPLxRZap4tBy09v2luESVaw_ndq2?g&q=J-UFpQ&BuQM+hHdAq) zk0ld*;rb;O%}{$9|{sg8CA z_G9&ZhA9Nd8VxmX*VM<58N|LksIz*}S@LBIImWAbgeF%@5cRLX7uI8AH5@+6?7 zi%rSAxT*b-d;%h=CLyREkY;YUZQ`7?3a%H%4{|%mLc-J0_H$;P><$nTKbA>wBL4X& z%k{5ZrxXsA7rBUxi;j9y)w)q^k#7hmuY&dN2ozmjN zt03Wj(IyFei~M@!=W&QDjPi;;7uilO(nX}MA&Bc1sR9GQsFThK9~;W%8~He9J;gkt zF~G^g_A_^gOk=c{$-BGYg!LF|zT+|)(wC6T4{K_aB5;n^B{=y0t7nPO9N*^MG4G#d zL4h889%W`bw&jrIvw3bIJGiSwMzH}3A%g+;U82RVZ+@wt8UK>x0GLfMs9M(0XIdhz zzIwAoZh-1?@B`cfbiiY=rf;gE1?yUl9W?U>#oiY_g;Oy|ory7(J?j9Zh5%1h?5@~s zEZHep32Pw1MP8xfNMn_Rx#ot z0aEFiX=Us3IfCqt_=&oGu9; z$Nx~AZq#$s?xu(B(K1PB`%HQ*j#PBbhNNkC%k5f{&3X|ofbk^oe`V~i+zEHvd#W3) z?2Ol_wE61^&d%Nk^Al9txN6Z_>A1Zr#1bt+;)Pbh>&`81g4%l78*3t!BsY$SAY*1M zVP}|yv3M)>hi@{qk$L~nv!V3k6eHl zZ|7W`v{obj)Yk%FvBCx)roS~W*7RnMXmsb`meY&@Y~UkjYI3RG1)b{7Ot9?I<(S5I ze(-%INSD|BGsA;tU$%cUo1=P;k8hjq>?JlS48Mne32inN))r|#!=?gS!tHr#eO_EY zn-U||lo!>+j=X_Z{QuZ{�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 B92)=t literal 0 HcmV?d00001 diff --git a/images/github-secrets.PNG b/images/github-secrets.PNG new file mode 100644 index 0000000000000000000000000000000000000000..27b50909f3bcd10274736fb76e06c2ea2058a9ac GIT binary patch literal 82390 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 OV?S9O$v@7P{q literal 0 HcmV?d00001 diff --git a/index.html b/index.html new file mode 100644 index 000000000..6835f3d83 --- /dev/null +++ b/index.html @@ -0,0 +1,1303 @@ + + + + + + + + + + + + + + + + + + + + + + BreatheCode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + +

Getting started

+

Working inside Docker (slower)

+

Build BreatheCode Dev docker image

+

Install docker desktop in your Windows, else find a guide to install Docker and Docker Compose in your linux distribution uname -a.

+
# Check which dependencies you need install in your operating system
+python -m scripts.doctor
+
+# Generate the BreatheCode Dev docker image
+docker-compose build bc-dev
+
+

Testing inside BreatheCode Dev

+
# Open the BreatheCode Dev, this shell don't export the port 8000
+docker-compose run bc-dev fish
+
+# Testing
+pipenv run test ./breathecode/activity  # path
+
+# Testing in parallel
+pipenv run ptest ./breathecode/activity  # path
+
+# Coverage
+pipenv run cov breathecode.activity  # python module path
+
+# Coverage in parallel
+pipenv run pcov breathecode.activity  # python module path
+
+

Run BreatheCode API as docker service

+
# open BreatheCode API as a service and export the port 8000
+docker-compose up -d bc-dev
+
+# open the BreatheCode Dev, this shell don't export the port 8000
+docker-compose run bc-dev fish
+
+# create super user
+pipenv run python manage.py createsuperuser
+
+# Close the BreatheCode Dev
+exit
+
+# See the output of Django
+docker-compose logs -f bc-dev
+
+# open localhost:8000 to view the api
+# open localhost:8000/admin to view the admin
+
+

Working in your local machine (recomended)

+

Installation in your local machine

+

Install docker desktop in your Windows, else find a guide to install Docker and Docker Compose in your linux distribution uname -a.

+
# Check which dependencies you need install in your operating system
+python -m scripts.doctor
+
+# Setting up the redis and postgres database, you also can install manually in your local machine this databases
+docker-compose up -d redis postgres
+
+# Install and setting up your development environment (this command replace your .env file)
+python -m scripts.install
+
+

Testing in your local machine

+
# Testing
+pipenv run test ./breathecode/activity  # path
+
+# Testing in parallel
+pipenv run ptest ./breathecode/activity  # path
+
+# Coverage
+pipenv run cov breathecode.activity  # python module path
+
+# Coverage in parallel
+pipenv run pcov breathecode.activity  # python module path
+
+

Run BreatheCode API in your local machine

+
# Collect statics
+pipenv run python manage.py collectstatic --noinput
+
+# Run migrations
+pipenv run python manage.py migrate
+
+# Load fixtures (populate the database)
+pipenv run python manage.py loaddata breathecode/*/fixtures/dev_*.json
+
+# Create super user
+pipenv run python manage.py createsuperuser
+
+# Run server
+pipenv run start
+
+# open localhost:8000 to view the api
+# open localhost:8000/admin to view the admin
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/installation/environment-variables/index.html b/installation/environment-variables/index.html new file mode 100644 index 000000000..74b173c2c --- /dev/null +++ b/installation/environment-variables/index.html @@ -0,0 +1,1137 @@ + + + + + + + + + + + + + + + + + + + + + + + + Environment variables - BreatheCode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + +

Environment variables

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
namedescription
ENVRepresents the current environment, can be DEVELOPMENT, TEST, and PRODUCTION
LOG_LEVELRepresents the log level for the logging module, can be NOTSET, DEBUG, INFO, WARNING, ERROR and CRITICAL
DATABASE_URLRepresents the connection string to the database, you can read more about schema url
CACHE_MIDDLEWARE_MINUTESRepresents how long an item will last in the cache
API_URLRepresents the url of api rest
ADMIN_URLRepresents the url of frontend of the admin
APP_URLRepresents the url of frontend of the webside
REDIS_URLRepresents the url of Redis
CELERY_TASK_SERIALIZERRepresents the default serialization method to use. Can be pickle json, yaml, msgpack or any custom serialization methods
EMAIL_NOTIFICATIONS_ENABLEDRepresents if the server can send notifications through email
SYSTEM_EMAILRepresents the email of Breathecode for support
SAVE_LEADSRepresents if Breathecode will persist the leads
COMPANY_NAMERepresents the company name
COMPANY_CONTACT_URLRepresents the company contact url
COMPANY_LEGAL_NAMERepresents the company legal name
COMPANY_ADDRESSRepresents the company address
MEDIA_GALLERY_BUCKETRepresents the bucket for the media gallery
DOWNLOADS_BUCKETRepresents the bucket for the CSV files
PROFILE_BUCKETRepresents the bucket for profile avatars
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/installation/fixtures/index.html b/installation/fixtures/index.html new file mode 100644 index 000000000..74b0ff302 --- /dev/null +++ b/installation/fixtures/index.html @@ -0,0 +1,1118 @@ + + + + + + + + + + + + + + + + + + + + + + + + Fixtures - BreatheCode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + +

Fixtures

+

Fixtures are fake data ideal for development.

+

Saving new fixtures

+
python manage.py dumpdata auth > ./breathecode/authenticate/fixtures/users.json
+
+

Loading all fixtures

+
pipenv run python manage.py loaddata breathecode/*/fixtures/dev_*.json
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/objects.inv b/objects.inv new file mode 100644 index 0000000000000000000000000000000000000000..124d74eb245f84f79de9508c7156723452be8d12 GIT binary patch literal 735 zcmV<50wDb(AX9K?X>NERX>N99Zgg*Qc_4OWa&u{KZXhxWBOp+6Z)#;@bUGkHa%Ew3 zXk|lhWMv8?AXa5^b7^mGIv_AEFfK3(BOp|0Wgv28ZDDC{WMy(7Z)PBLXlZjGW@&6? zAZc?TV{dJ6a%FRKWn>_Ab7^j8AbMhVOlfmD+2JT2(#t*i?Oh z^Z_id8NgbDiDyX5+t-F=L#nWBiJ4wjEd2Z*o*54|M-PfB&@Kmo;Gc{J6s2>e1*xs; za)IQ$D&f%fUu&0l77iZ|lQNqtv$HaL|KV`=?s%)Q`1uUbRw3@>FA8G^NYOQ#jBEa# zLx6;$Hor}SVs=3er-W9zdKss9gK5fayd^v8yFs(fS=>+lQ<#$3hkvKF2alR<&fu;ZjzszNi4&TG1>RvYS8Tw^(C5H%LOCYL@XBBB;)86ne;m>UbhV2o;)ue# zTE?T0GE%ohFcq1coUNn?O7Q#P3+HP^E9o0W8@)fepd05jjmyzTYn0|YhSQ-B8AE;_d`L*JFdNs&#JBN;^CQcB`s+n zLUrMKLq^}Si%A8oRnvtAF=Iy}7d?JGsX*o6aX04d%=Kz{i?jIllCoGWsUV^PySe~a zggJe}DH-^BRH9_~odS=yxe3*_wwKdiIc`^F6fHe&D%7}f+@BLGmIcJYXcg&-hvbka RKW)duIUj6d{Q*!I90Vw7S*!p6 literal 0 HcmV?d00001 diff --git a/search/search_index.json b/search/search_index.json new file mode 100644 index 000000000..e5a763503 --- /dev/null +++ b/search/search_index.json @@ -0,0 +1 @@ +{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Getting started","text":""},{"location":"#working-inside-docker-slower","title":"Working inside Docker (slower)","text":""},{"location":"#build-breathecode-dev-docker-image","title":"Build BreatheCode Dev docker image","text":"

Install docker desktop in your Windows, else find a guide to install Docker and Docker Compose in your linux distribution uname -a.

# Check which dependencies you need install in your operating system\npython -m scripts.doctor\n\n# Generate the BreatheCode Dev docker image\ndocker-compose build bc-dev\n
"},{"location":"#testing-inside-breathecode-dev","title":"Testing inside BreatheCode Dev","text":"
# Open the BreatheCode Dev, this shell don't export the port 8000\ndocker-compose run bc-dev fish\n\n# Testing\npipenv run test ./breathecode/activity  # path\n# Testing in parallel\npipenv run ptest ./breathecode/activity  # path\n# Coverage\npipenv run cov breathecode.activity  # python module path\n# Coverage in parallel\npipenv run pcov breathecode.activity  # python module path\n
"},{"location":"#run-breathecode-api-as-docker-service","title":"Run BreatheCode API as docker service","text":"
# open BreatheCode API as a service and export the port 8000\ndocker-compose up -d bc-dev\n\n# open the BreatheCode Dev, this shell don't export the port 8000\ndocker-compose run bc-dev fish\n\n# create super user\npipenv run python manage.py createsuperuser\n\n# Close the BreatheCode Dev\nexit\n# See the output of Django\ndocker-compose logs -f bc-dev\n\n# open localhost:8000 to view the api\n# open localhost:8000/admin to view the admin\n
"},{"location":"#working-in-your-local-machine-recomended","title":"Working in your local machine (recomended)","text":""},{"location":"#installation-in-your-local-machine","title":"Installation in your local machine","text":"

Install docker desktop in your Windows, else find a guide to install Docker and Docker Compose in your linux distribution uname -a.

# Check which dependencies you need install in your operating system\npython -m scripts.doctor\n\n# Setting up the redis and postgres database, you also can install manually in your local machine this databases\ndocker-compose up -d redis postgres\n\n# Install and setting up your development environment (this command replace your .env file)\npython -m scripts.install\n
"},{"location":"#testing-in-your-local-machine","title":"Testing in your local machine","text":"
# Testing\npipenv run test ./breathecode/activity  # path\n# Testing in parallel\npipenv run ptest ./breathecode/activity  # path\n# Coverage\npipenv run cov breathecode.activity  # python module path\n# Coverage in parallel\npipenv run pcov breathecode.activity  # python module path\n
"},{"location":"#run-breathecode-api-in-your-local-machine","title":"Run BreatheCode API in your local machine","text":"
# Collect statics\npipenv run python manage.py collectstatic --noinput\n\n# Run migrations\npipenv run python manage.py migrate\n\n# Load fixtures (populate the database)\npipenv run python manage.py loaddata breathecode/*/fixtures/dev_*.json\n\n# Create super user\npipenv run python manage.py createsuperuser\n\n# Run server\npipenv run start\n\n# open localhost:8000 to view the api\n# open localhost:8000/admin to view the admin\n
"},{"location":"endponts/","title":"Enpoints Documentation","text":"
  • Postman
  • OpenApi
"},{"location":"apps/activities/","title":"Activities","text":""},{"location":"apps/activities/#activity-api","title":"Activity API","text":"

This API uses Google DataStore as storage, there is not local storage on Heroku or Postgres.

We need Google DataStore because we plan to store huge amounts of activities that the user can do inside breathecode.

Possible activities (so far):

\"breathecode_login\" //every time it logs in\n\"online_platform_registration\" //first day using breathecode\n\"public_event_attendance\" //attendy on an eventbrite event\n\"classroom_attendance\" //when the student attent to class\n\"classroom_unattendance\" //when the student miss class\n\"lesson_opened\" //when a lessons is opened on the platform\n\"office_attendance\" //when the office raspberry pi detects the student\n\"nps_survey_answered\" //when a nps survey is answered by the student\n\"exercise_success\" //when student successfully tests exercise\n

Any activity has the following inputs:

    'cohort',\n    'data',\n    'day',\n    'slug',\n    'user_agent',\n
"},{"location":"apps/activities/#endpoints-for-the-user","title":"Endpoints for the user","text":"

Get recent user activity

GET: activity/user/{email_or_id}?slug=activity_slug\n

Add a new user activity (requires authentication)

POST: activity/user/{email_or_id}\n{\n    'slug' => 'activity_slug',\n    'data' => 'any aditional data (string or json-encoded-string)'\n}\n\n\ud83d\udca1 Node: You can pass the cohort in the data json object and it will be possible to filter on the activity graph like this:\n\n{\n    'slug' => 'activity_slug',\n    'data' => \"{ \\\"cohort\\\": \\\"mdc-iii\\\" }\" (json encoded string with the cohort id)\n}\n

Endpoints for the Cohort

Get recent user activity

GET: activity/cohort/{slug_or_id}?slug=activity_slug\n
Endpoints for the coding_error's
Get recent user coding_errors\nGET: activity/coding_error/{email_or_id}?slug=activity_slug\n
Add a new coding_error (requires authentication)\nPOST: activity/coding_error/\n\n{\n    \"user_id\" => \"my@email.com\",\n    \"slug\" => \"webpack_error\",\n    \"data\" => \"optional additional information about the error\",\n    \"message\" => \"file not found\",\n    \"name\" => \"module-not-found,\n    \"severity\" => \"900\",\n    \"details\" => \"stack trace for the error as string\"\n}\n

"},{"location":"apps/admissions/","title":"BreatheCode.Admissions","text":"

This module take care of the academic side of breathecode: Students, Cohorts, Course (aka: Certificate), Syllabus, etc. These are some of the things you can do with the breathecode.admissions API:

  1. Manage Academies (BreatheCode let's you divide the academic operations into several academies normally based on territory, for example: 4Geeks Academy Miami vs 4Geeks Academy Madrid).
  2. Manage Academy Staff: There are multiple roles surroing an academy, here you can invite users to one or many academies and assign them roles based on their responsabilities.
  3. Manage Students (invite and delete students).
  4. Manage Cohorts: Every new batch of students that starts in a classroom with a start and end date is called a \"Cohort\".

TODO: finish this documentation.

"},{"location":"apps/admissions/#commands","title":"Commands","text":""},{"location":"apps/admissions/#sync-academies","title":"Sync academies","text":"
python manage.py sync_admissions academies\n

Override previous academies

python manage.py sync_admissions academies --override\n

"},{"location":"apps/admissions/#sync-courses","title":"Sync courses","text":"
python manage.py sync_admissions certificates\n
"},{"location":"apps/admissions/#sync-cohorts","title":"Sync cohorts","text":"
python manage.py sync_admissions cohorts\n
"},{"location":"apps/admissions/#sync-students","title":"Sync students","text":"

python manage.py sync_admissions students --limit=3\n
Limit: the number of students to sync

"},{"location":"apps/monitoring/introduction/","title":"Intro to monitoring","text":"

This app is ideal for running diagnostic and reminders on the breathecode platform.

"},{"location":"apps/monitoring/introduction/#installation","title":"Installation","text":"
  • Setup the monitor app job for once a day, this is the command:

    $ python manage.py monitor apps\n

  • Setup the monitor script job for once a day, this is the command:

    $ python manage.py monitor script\n

"},{"location":"apps/monitoring/scripts/","title":"Monitoring Scripts","text":"

A monitoring script is something that you want to execute recurrently withing the breathecode API, for example:

scripts/alert_pending_leads.py is a small python script that checks if there is FormEntry Marketing module database that are pending processing.

You can create a monitoring script to remind academy staff members about things, or to remind students about pending homework, etc.

"},{"location":"apps/monitoring/scripts/#stepts-to-create-a-new-script","title":"Stepts to create a new script:","text":"
  1. create a new python file inside ./breathecode/monitoring/scripts
  2. make sure your script starts with this content always:
#!/usr/bin/env python\n\"\"\"\nAlert when there are Form Entries with status = PENDING\n\"\"\"\nfrom breathecode.utils import ScriptNotification\n# start your code here\n
  1. You have access to the entire breathecode API from here, you can import models, services or any other class or variable from any file.
  2. You can raise a ScriptNotification to notify for MINOR or CRITICAL reasons, for example:

# here we are raising a notification because there are 2 pending tasks\nraise ScriptNotification(\"There are 2 pending taks\", status='MINOR', slug=\"pending_tasks\")\n
5. If you don't raise any ScriptNotification and there are no other Exceptions in the script, it will be considered successfull and no notifications will trigger. 6. When a ScriptNotification has been raise the Application owner will receive a notification to the application.email and slack channel configured for notifications. 7. Check for other scripts as examples. 8. Test your script.

"},{"location":"apps/monitoring/scripts/#global-context","title":"Global Context","text":"

There are some global variables that you have available during your scripts:

Variable name Value academy Contains the academy model object, you can use it to retrieve the current academy id like this: query.filter(academy__id=academy.id)"},{"location":"apps/monitoring/scripts/#manually-running-your-script","title":"Manually running your script","text":"

You can test your scripts by running the following command:

$ python manage.py run_script <file_name>\n\n# For example you can test the alert_pending_leads script like this:\n$ python manage.py run_script alert_pending_leads.py\n
"},{"location":"apps/monitoring/scripts/#example-script","title":"Example Script","text":"

The following script checks for pending leads to process:

#!/usr/bin/env python\n\"\"\"\nAlert when there are Form Entries with status = PENDING\n\"\"\"\nfrom breathecode.marketing.models import FormEntry\nfrom django.db.models import Q\nfrom breathecode.utils import ScriptNotification\n# check the database for pending leads\npending_leads = FormEntry.objects.filter(storage_status=\"PENDING\").filter(Q(academy__id=academy.id) | Q(location=academy.slug))\n# trigger notification because pending leads were found\nif len(pending_leads) > 0:\nraise ScriptNotification(f\"Warning there are {len(pending_leads)} pending form entries\", status='MINOR')\n# You can print this and it will show on the script results\nprint(\"No pending leads\")\n
"},{"location":"apps/monitoring/scripts/#unit-testing-your-script","title":"Unit testing your script","text":"

from breathecode.monitoring.actions import run_script

script = run_script(model.monitor_script)\ndel script['slack_payload']\ndel script['title']\nexpected = {'details': script['details'],\n'severity_level': 5,\n'status': script['status'],\n'text': script['text']\n}\nself.assertEqual(script, expected)\nself.assertEqual(self.all_monitor_script_dict(), [{\n**self.model_to_dict(model, 'monitor_script'),\n}])\n
"},{"location":"deployment/configuring-the-github-secrets/","title":"Configuring the Github secrets","text":"
  1. Get Dockerhub token
  2. Add the repo to Coveralls https://coveralls.io/repos/new
  3. Add the repo to Codecov https://app.codecov.io/gh/+
  4. Set up the secrets
"},{"location":"deployment/environment-variables/","title":"Environment variables","text":"name description ENV Represents the current environment, can be DEVELOPMENT, TEST, and PRODUCTION LOG_LEVEL Represents the log level for the logging module, can be NOTSET, DEBUG, INFO, WARNING, ERROR and CRITICAL DATABASE_URL Represents the connection string to the database, you can read more about schema url CACHE_MIDDLEWARE_MINUTES Represents how long an item will last in the cache API_URL Represents the url of api rest ADMIN_URL Represents the url of frontend of the admin APP_URL Represents the url of frontend of the webside REDIS_URL Represents the url of Redis CELERY_TASK_SERIALIZER Represents the default serialization method to use. Can be pickle json, yaml, msgpack or any custom serialization methods EMAIL_NOTIFICATIONS_ENABLED Represents if the server can send notifications through email SYSTEM_EMAIL Represents the email of Breathecode for support GITHUB_CLIENT_ID Represents the client id used for the OAuth2 with Github GITHUB_SECRET Represents the secret used for the OAuth2 with Github GITHUB_REDIRECT_URL Represents the redirect url used for the OAuth2 with Github SLACK_CLIENT_ID Represents the client id used for the OAuth2 with Slack SLACK_SECRET Represents the secret used for the OAuth2 with Slack SLACK_REDIRECT_URL Represents the redirect url used for the OAuth2 with Slack MAILGUN_API_KEY Represents the api key used for the OAuth2 with Mailgun MAILGUN_DOMAIN Represents the domain of Breathecode that provided Mailgun EVENTBRITE_KEY Represents the key used for the OAuth2 with Eventbrite FACEBOOK_VERIFY_TOKEN Represents the verify token used for the OAuth2 with Facebook FACEBOOK_CLIENT_ID Represents the client id used for the OAuth2 with Facebook FACEBOOK_SECRET Represents the secret used for the OAuth2 with Facebook FACEBOOK_REDIRECT_URL Represents the redirect url used for the OAuth2 with Facebook ACTIVE_CAMPAIGN_KEY Represents the key used for the OAuth2 with Active Campaign ACTIVE_CAMPAIGN_URL Represents the domain of Breathecode that provided Active Campaign GOOGLE_APPLICATION_CREDENTIALS Represents the file will be saved the service account of Google Cloud GOOGLE_SERVICE_KEY Represents the content of the service account used for the OAuth2 with Google Cloud GOOGLE_PROJECT_ID Project ID on google cloud used for the integration of the entire API GOOGLE_CLOUD_KEY Represents the key used for the OAuth2 with Google Cloud GOOGLE_CLIENT_ID Represents the client id used for the OAuth2 with Google Cloud GOOGLE_SECRET Represents the secret used for the OAuth2 with Google Cloud GOOGLE_REDIRECT_URL Represents the redirect url used for the OAuth2 with Google Cloud DAILY_API_KEY Represents the api key used for the OAuth2 with Daily DAILY_API_URL Represents the domain of Breathecode that provided Daily SAVE_LEADS Represents if Breathecode will persist the leads COMPANY_NAME Represents the company name COMPANY_CONTACT_URL Represents the company contact url COMPANY_LEGAL_NAME Represents the company legal name COMPANY_ADDRESS Represents the company address MEDIA_GALLERY_BUCKET Represents the bucket for the media gallery DOWNLOADS_BUCKET Represents the bucket for the CSV files PROFILE_BUCKET Represents the bucket for profile avatars"},{"location":"installation/environment-variables/","title":"Environment variables","text":"name description ENV Represents the current environment, can be DEVELOPMENT, TEST, and PRODUCTION LOG_LEVEL Represents the log level for the logging module, can be NOTSET, DEBUG, INFO, WARNING, ERROR and CRITICAL DATABASE_URL Represents the connection string to the database, you can read more about schema url CACHE_MIDDLEWARE_MINUTES Represents how long an item will last in the cache API_URL Represents the url of api rest ADMIN_URL Represents the url of frontend of the admin APP_URL Represents the url of frontend of the webside REDIS_URL Represents the url of Redis CELERY_TASK_SERIALIZER Represents the default serialization method to use. Can be pickle json, yaml, msgpack or any custom serialization methods EMAIL_NOTIFICATIONS_ENABLED Represents if the server can send notifications through email SYSTEM_EMAIL Represents the email of Breathecode for support SAVE_LEADS Represents if Breathecode will persist the leads COMPANY_NAME Represents the company name COMPANY_CONTACT_URL Represents the company contact url COMPANY_LEGAL_NAME Represents the company legal name COMPANY_ADDRESS Represents the company address MEDIA_GALLERY_BUCKET Represents the bucket for the media gallery DOWNLOADS_BUCKET Represents the bucket for the CSV files PROFILE_BUCKET Represents the bucket for profile avatars"},{"location":"installation/fixtures/","title":"Fixtures","text":"

Fixtures are fake data ideal for development.

"},{"location":"installation/fixtures/#saving-new-fixtures","title":"Saving new fixtures","text":"
python manage.py dumpdata auth > ./breathecode/authenticate/fixtures/users.json\n
"},{"location":"installation/fixtures/#loading-all-fixtures","title":"Loading all fixtures","text":"
pipenv run python manage.py loaddata breathecode/*/fixtures/dev_*.json\n
"},{"location":"security/capabilities/","title":"Capabilities","text":"

Authenticated users must belong to at least one academy with a specific role, each role has a series of capabilities that specify what any user with that role will be \"capable\" of doing.

Authenticated methods must be decorated with the @capable_of decorator in increase security validation. For example:

    from breathecode.utils import capable_of\n@capable_of('crud_member')\ndef post(self, request, academy_id=None):\nserializer = StaffPOSTSerializer(data=request.data)\nif serializer.is_valid():\nserializer.save()\nreturn Response(serializer.data, status=status.HTTP_201_CREATED)\nreturn Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)\n

Any view decorated with the @capable_of must be used passing an academy id either:

  1. Anywhere on the endpoint url, E.g: path('academy/<int:academy_id>/member', MemberView.as_view()),
  2. Or on the request header using the Academy header.
"},{"location":"security/capabilities/#available-capabilities","title":"Available capabilities:","text":"

This list is alive, it will grow and vary over time:

CAPABILITIES = [\n{\n'slug': 'read_my_academy',\n'description': 'Read your academy information'\n},\n{\n'slug': 'crud_my_academy',\n'description': 'Read, or update your academy information (very high level, almost the academy admin)'\n},\n{\n'slug': 'crud_member',\n'description': 'Create, update or delete academy members (very high level, almost the academy admin)'\n},\n{\n'slug': 'read_member',\n'description': 'Read academy staff member information'\n},\n{\n'slug': 'crud_student',\n'description': 'Create, update or delete students'\n},\n{\n'slug': 'read_student',\n'description': 'Read student information'\n},\n{\n'slug': 'read_invite',\n'description': 'Read invites from users'\n},\n{\n'slug': 'crud_invite',\n'description': 'Create, update or delete invites from users'\n},\n{\n'slug': 'invite_resend',\n'description': 'Resent invites for user academies'\n},\n{\n'slug': 'read_assignment',\n'description': 'Read assignment information'\n},\n{\n'slug':\n'read_assignment_sensitive_details',\n'description':\n'The mentor in residence is allowed to see aditional info about the task, like the \"delivery url\"'\n},\n{\n'slug': 'read_shortlink',\n'description': 'Access the list of marketing shortlinks'\n},\n{\n'slug': 'crud_shortlink',\n'description': 'Create, update and delete marketing short links'\n},\n{\n'slug': 'crud_assignment',\n'description': 'Update assignments'\n},\n{\n'slug': 'task_delivery_details',\n'description': 'Get delivery URL for a task, that url can be sent to students for delivery'\n},\n{\n'slug': 'read_certificate',\n'description': 'List and read all academy certificates'\n},\n{\n'slug': 'crud_certificate',\n'description': 'Create, update or delete student certificates'\n},\n{\n'slug': 'read_layout',\n'description': 'Read layouts to generate new certificates'\n},\n{\n'slug': 'read_syllabus',\n'description': 'List and read syllabus information'\n},\n{\n'slug': 'crud_syllabus',\n'description': 'Create, update or delete syllabus versions'\n},\n{\n'slug': 'read_organization',\n'description': 'Read academy organization details'\n},\n{\n'slug': 'crud_organization',\n'description': 'Update, create or delete academy organization details'\n},\n{\n'slug': 'read_event',\n'description': 'List and retrieve event information'\n},\n{\n'slug': 'crud_event',\n'description': 'Create, update or delete event information'\n},\n{\n'slug': 'read_all_cohort',\n'description': 'List all the cohorts or single cohort information'\n},\n{\n'slug': 'read_single_cohort',\n'description': 'single cohort information related to a user'\n},\n{\n'slug': 'crud_cohort',\n'description': 'Create, update or delete cohort info'\n},\n{\n'slug': 'read_eventcheckin',\n'description': 'List and read all the event_checkins'\n},\n{\n'slug': 'read_survey',\n'description': 'List all the nps answers'\n},\n{\n'slug': 'crud_survey',\n'description': 'Create, update or delete surveys'\n},\n{\n'slug': 'read_nps_answers',\n'description': 'List all the nps answers'\n},\n{\n'slug': 'read_lead',\n'description': 'List all the leads'\n},\n{\n'slug': 'read_won_lead',\n'description': 'List all the won leads'\n},\n{\n'slug': 'crud_lead',\n'description': 'Create, update or delete academy leads'\n},\n{\n'slug': 'read_review',\n'description': 'Read review for a particular academy'\n},\n{\n'slug': 'crud_review',\n'description': 'Create, update or delete academy reviews'\n},\n{\n'slug': 'read_media',\n'description': 'List all the medias'\n},\n{\n'slug': 'crud_media',\n'description': 'Create, update or delete academy medias'\n},\n{\n'slug': 'read_media_resolution',\n'description': 'List all the medias resolutions'\n},\n{\n'slug': 'crud_media_resolution',\n'description': 'Create, update or delete academy media resolutions'\n},\n{\n'slug': 'read_cohort_activity',\n'description': 'Read low level activity in a cohort (attendancy, etc.)'\n},\n{\n'slug': 'generate_academy_token',\n'description': 'Create a new token only to be used by the academy'\n},\n{\n'slug': 'get_academy_token',\n'description': 'Read the academy token'\n},\n{\n'slug': 'send_reset_password',\n'description': 'Generate a temporal token and resend forgot password link'\n},\n{\n'slug': 'read_activity',\n'description': 'List all the user activities'\n},\n{\n'slug': 'crud_activity',\n'description': 'Create, update or delete a user activities'\n},\n{\n'slug': 'read_assignment',\n'description': 'List all the assignments'\n},\n{\n'slug': 'crud_assignment',\n'description': 'Create, update or delete a assignment'\n},\n{\n'slug':\n'classroom_activity',\n'description':\n'To report student activities during the classroom or cohorts (Specially meant for teachers)'\n},\n{\n'slug': 'academy_reporting',\n'description': 'Get detailed reports about the academy activity'\n},\n{\n'slug': 'generate_temporal_token',\n'description': 'Generate a temporal token to reset github credential or forgot password'\n},\n{\n'slug': 'read_mentorship_service',\n'description': 'Get all mentorship services from one academy'\n},\n{\n'slug': 'crud_mentorship_service',\n'description': 'Create, delete or update all mentorship services from one academy'\n},\n{\n'slug': 'read_mentorship_mentor',\n'description': 'Get all mentorship mentors from one academy'\n},\n{\n'slug': 'crud_mentorship_mentor',\n'description': 'Create, delete or update all mentorship mentors from one academy'\n},\n{\n'slug': 'read_mentorship_session',\n'description': 'Get all session from one academy'\n},\n{\n'slug': 'crud_mentorship_session',\n'description': 'Create, delete or update all session from one academy'\n},\n{\n'slug': 'crud_freelancer_bill',\n'description': 'Create, delete or update all freelancer bills from one academy'\n},\n{\n'slug': 'read_freelancer_bill',\n'description': 'Read all all freelancer bills from one academy'\n},\n{\n'slug': 'crud_mentorship_bill',\n'description': 'Create, delete or update all mentroship bills from one academy'\n},\n{\n'slug': 'read_mentorship_bill',\n'description': 'Read all mentroship bills from one academy'\n},\n{\n'slug': 'read_asset',\n'description': 'Read all academy registry assets'\n},\n{\n'slug': 'crud_asset',\n'description': 'Update, create and delete registry assets'\n},\n{\n'slug': 'read_tag',\n'description': 'Read marketing tags and their details'\n},\n{\n'slug': 'crud_tag',\n'description': 'Update, create and delete a marketing tag and its details'\n},\n{\n'slug': 'get_gitpod_user',\n'description': 'List gitpod user the academy is consuming'\n},\n{\n'slug': 'update_gitpod_user',\n'description': 'Update gitpod user expiration based on available information'\n},\n{\n'slug': 'read_technology',\n'description': 'Read asset technologies'\n},\n{\n'slug': 'crud_technology',\n'description': 'Update, create and delete asset technologies'\n},\n{\n'slug': 'read_keyword',\n'description': 'Read SEO keywords'\n},\n{\n'slug': 'crud_keyword',\n'description': 'Update, create and delete SEO keywords'\n},\n{\n'slug': 'read_keywordcluster',\n'description': 'Update, create and delete asset technologies'\n},\n{\n'slug': 'crud_keywordcluster',\n'description': 'Update, create and delete asset technologies'\n},\n]\n
"},{"location":"services/google_cloud/google-cloud-functions/","title":"Google Cloud Functions","text":""},{"location":"services/google_cloud/google-cloud-functions/#write-a-http-function","title":"Write a HTTP function","text":"

https://cloud.google.com/functions/docs/writing/http

"},{"location":"services/google_cloud/google-cloud-functions/#see-active-functions","title":"See active functions","text":"

https://console.cloud.google.com/functions/list

"},{"location":"services/google_cloud/google-cloud-functions/#testing-function","title":"Testing function","text":"

https://cloud.google.com/functions/docs/testing/test-http#functions-testing-http-integration-python

"},{"location":"services/google_cloud/google-cloud-functions/#list-functions","title":"List functions","text":"Name Activator Resource Repository process-zap HTTP process-zap screenshots HTTP screenshots jefer94/screenshots resize-image HTTP resize-image breatheco-de/gcloud-resize-image thumbnail-generator Bucket media-breathecode breatheco-de/gcloud-thumbnail-generator thumbnail-generator-dev Bucket media-breathecode-dev breatheco-de/gcloud-thumbnail-generator"},{"location":"services/google_cloud/storage/","title":"Storage","text":""},{"location":"services/google_cloud/storage/#breathecode.services.google_cloud.storage.Storage","title":"Storage","text":"

Google Cloud Storage

Source code in breathecode/services/google_cloud/storage.py
class Storage:\n\"\"\"Google Cloud Storage\"\"\"\nclient: storage.Client\ndef __init__(self) -> None:\n# from google.cloud.storage import Client\ncredentials.resolve_credentials()\nself.client = storage.Client()\ndef file(self, bucket_name: str, file_name: str) -> File:\n\"\"\"Get File object\n        Args:\n            bucket_name (str): Name of bucket in Google Cloud Storage\n            file_name (str): Name of blob in Google Cloud Bucket\n        Returns:\n            File: File object\n        \"\"\"\nbucket = self.client.bucket(bucket_name)\nreturn File(bucket, file_name)\n
"},{"location":"services/google_cloud/storage/#breathecode.services.google_cloud.storage.Storage.file","title":"file(bucket_name, file_name)","text":"

Get File object

Parameters:

Name Type Description Default bucket_name str

Name of bucket in Google Cloud Storage

required file_name str

Name of blob in Google Cloud Bucket

required

Returns:

Name Type Description File File

File object

Source code in breathecode/services/google_cloud/storage.py
def file(self, bucket_name: str, file_name: str) -> File:\n\"\"\"Get File object\n    Args:\n        bucket_name (str): Name of bucket in Google Cloud Storage\n        file_name (str): Name of blob in Google Cloud Bucket\n    Returns:\n        File: File object\n    \"\"\"\nbucket = self.client.bucket(bucket_name)\nreturn File(bucket, file_name)\n
"},{"location":"services/slack%20integration/icons/","title":"Icons","text":"

The following icons are being used for the slack integrations https://www.pngrepo.com/collection/soft-colored-ui-icons/1

"},{"location":"signals/quickstart/","title":"Quickstart","text":""},{"location":"signals/quickstart/#signals","title":"Signals","text":"

The official documentation for django signals can be found here.

At BreatheCode, signals are similar concept to \"events\", we use signals as custom \"events\" that can notify important things that happen in one app to all the other app's (if they are listening).

For example: When a student drops from a cohort

There is a signal to notify when a student educational status gets updated, this is useful because other application may react to it. Here is the signal being initialized, here is being triggered/dispatched when a student gets saved and this is an example where the signal is being received on the breathecode.marketing.app to trigger some additional tasks within the system.

"},{"location":"signals/quickstart/#when-to-use-a-signal","title":"When to use a signal","text":"

Inside the breathecode team, we see signals for asynchronous processing of any side effects, we try to focus on them for communication between apps only.

"},{"location":"signals/quickstart/#declare-a-new-signal","title":"Declare a new signal","text":"

You have many examples that you can find inside the code, each breathecode app has a file signals.py that contains all the signals dispatched by that app. If the file does not exist within one of the apps, and you need to create a signal for that app, you can create the file yourself.

If you wanted to create a signal for when a cohort is saved, you should start by initializing it inside breathecode/admissions/signals.py like this:

from django.dispatch import Signal\ncohort_saved = Signal()\n
"},{"location":"signals/quickstart/#dispatching-a-signal","title":"Dispatching a signal","text":"

All the initialized signals are available on the same application signals.py file, if the signal you want to dispatch is not there, you should probably declare a new one.

After the signal is initialized, it can be dispatched anywhere withing the same app, for example inside a serializer create method like this:

from .signals import cohort_saved\nclass CohortSerializer(CohortSerializerMixin):\ndef create(self, validated_data):\ncohort = Cohort.objects.create(**validated_data, **self.context)\ncohort_saved.send(instance=self, sender=CohortUser)\nreturn cohort\n
"},{"location":"signals/quickstart/#receiving-a-signal","title":"Receiving a signal","text":"

All django applications can subscribe to receive a signal, even if those signals are coming from another app, but you should always add your receiving code inside the receivers.py of the app that will react to the signal.

The following code will receive the cohort_saved signal and print on the screen if its being created or updated.

Note: Its a good idea to always connect receivers to tasks, that way you can asynconosly pospone any processing that you will do after the cohort its created.

from breathecode.admissions.signals import student_edu_status_updated, cohort_saved\nfrom .models import FormEntry, ActiveCampaignAcademy\nfrom .tasks import add_cohort_task_to_student, add_cohort_slug_as_acp_tag\n@receiver(cohort_saved, sender=Cohort)\ndef cohort_post_save(sender, instance, created, *args, **kwargs):\nif created:\nprint(f\"The cohort {instance.id} was just created\")\n# you can call a task from task.py here.\nelse:\nprint(f\"The cohort {instance.id} was just updated\")\n
"},{"location":"testing/runing-tests/","title":"Runing tests","text":""},{"location":"testing/runing-tests/#run-tests","title":"Run tests","text":"
pipenv run test ./breathecode/\n
"},{"location":"testing/runing-tests/#run-tests-in-parallel","title":"Run tests in parallel","text":"
pipenv run ptest ./breathecode/\n
"},{"location":"testing/runing-tests/#run-coverage","title":"Run coverage","text":"
pipenv run cov breathecode\n
"},{"location":"testing/runing-tests/#run-coverage-in-parallel","title":"Run coverage in parallel","text":"
pipenv run pcov breathecode\n
"},{"location":"testing/runing-tests/#testing-inside-docker-fallback-option","title":"Testing inside Docker (fallback option)","text":"
  1. Check which dependencies you need install in you operating system pipenv run doctor or python -m scripts.doctor.
  2. Install docker desktop in your Windows, else find a guide to install Docker and Docker Compose in your linux distribution uname -a.
  3. Generate the BreatheCode Shell image with pipenv run docker_build_shell.
  4. Run BreatheCode Shell with docker-compose run bc-shell
  5. Run pipenv run test, pipenv run ptest, pipenv run cov or pipenv run pcov.
"},{"location":"testing/mixins/bc-cache/","title":"bc.cache","text":""},{"location":"testing/mixins/bc-cache/#breathecode.tests.mixins.breathecode_mixin.cache.Cache","title":"Cache","text":"

Mixin with the purpose of cover all the related with cache

Source code in breathecode/tests/mixins/breathecode_mixin/cache.py
class Cache:\n\"\"\"Mixin with the purpose of cover all the related with cache\"\"\"\nclear = CacheMixin.clear_cache\n_parent: APITestCase\n_bc: interfaces.BreathecodeInterface\ndef __init__(self, parent, bc: interfaces.BreathecodeInterface) -> None:\nself._parent = parent\nself._bc = bc\n
"},{"location":"testing/mixins/bc-check/","title":"bc.check","text":""},{"location":"testing/mixins/bc-check/#breathecode.tests.mixins.breathecode_mixin.check.Check","title":"Check","text":"

Mixin with the purpose of cover all the related with the custom asserts

Source code in breathecode/tests/mixins/breathecode_mixin/check.py
class Check:\n\"\"\"Mixin with the purpose of cover all the related with the custom asserts\"\"\"\nsha256 = Sha256Mixin.assertHash\ntoken = TokenMixin.assertToken\n_parent: APITestCase\n_bc: interfaces.BreathecodeInterface\ndef __init__(self, parent, bc: interfaces.BreathecodeInterface) -> None:\nself._parent = parent\nself._bc = bc\ndef datetime_in_range(self, start: datetime, end: datetime, date: datetime) -> None:\n\"\"\"\n        Check if a range if between start and end argument.\n        Usage:\n        ```py\n        from django.utils import timezone\n        start = timezone.now()\n        in_range = timezone.now()\n        end = timezone.now()\n        out_of_range = timezone.now()\n        # pass because this datetime is between start and end\n        self.bc.check.datetime_in_range(start, end, in_range)  # \ud83d\udfe2\n        # fail because this datetime is not between start and end\n        self.bc.check.datetime_in_range(start, end, out_of_range)  # \ud83d\udd34\n        ```\n        \"\"\"\nself._parent.assertLess(start, date)\nself._parent.assertGreater(end, date)\ndef partial_equality(self, first: dict | list[dict], second: dict | list[dict]) -> None:\n\"\"\"\n        Fail if the two objects are partially unequal as determined by the '==' operator.\n        Usage:\n        ```py\n        obj1 = {'key1': 1, 'key2': 2}\n        obj2 = {'key2': 2, 'key3': 1}\n        obj3 = {'key2': 2}\n        # it's fail because the key3 is not in the obj1\n        self.bc.check.partial_equality(obj1, obj2)  # \ud83d\udd34\n        # it's fail because the key1 is not in the obj2\n        self.bc.check.partial_equality(obj2, obj1)  # \ud83d\udd34\n        # it's pass because the key2 exists in the obj1\n        self.bc.check.partial_equality(obj1, obj3)  # \ud83d\udfe2\n        # it's pass because the key2 exists in the obj2\n        self.bc.check.partial_equality(obj2, obj3)  # \ud83d\udfe2\n        # it's fail because the key1 is not in the obj3\n        self.bc.check.partial_equality(obj3, obj1)  # \ud83d\udd34\n        # it's fail because the key3 is not in the obj3\n        self.bc.check.partial_equality(obj3, obj2)  # \ud83d\udd34\n        ```\n        \"\"\"\nassert type(first) == type(second)\nif isinstance(first, list):\nassert len(first) == len(second)\noriginal = []\nfor i in range(0, len(first)):\noriginal.append(self._fill_partial_equality(first[i], second[i]))\nelse:\noriginal = self._fill_partial_equality(first, second)\nself._parent.assertEqual(original, second)\ndef calls(self, first: list[call], second: list[call]) -> None:\n\"\"\"\n        Fail if the two objects are partially unequal as determined by the '==' operator.\n        Usage:\n        ```py\n        self.bc.check.calls(mock.call_args_list, [call(1, 2, a=3, b=4)])\n        ```\n        \"\"\"\n# assert len(first) == len(second), f'not have same length than {first}\\n{second}'\nself._parent.assertEqual(len(first),\nlen(second),\nmsg=f'Does not have same length\\n\\n{first}\\n\\n!=\\n\\n{second}')\nfor i in range(0, len(first)):\nself._parent.assertEqual(first[i].args, second[i].args, msg=f'args in index {i} does not match')\nself._parent.assertEqual(first[i].kwargs,\nsecond[i].kwargs,\nmsg=f'kwargs in index {i} does not match')\ndef _fill_partial_equality(self, first: dict, second: dict) -> dict:\noriginal = {}\nfor key in second.keys():\noriginal[key] = second[key]\nreturn original\ndef queryset_of(self, query: Any, model: Model) -> None:\n\"\"\"\n        Check if the first argument is a queryset of a models provided as second argument.\n        Usage:\n        ```py\n        from breathecode.admissions.models import Cohort, Academy\n        self.bc.database.create(cohort=1)\n        collection = []\n        queryset = Cohort.objects.filter()\n        # pass because the first argument is a QuerySet and it's type Cohort\n        self.bc.check.queryset_of(queryset, Cohort)  # \ud83d\udfe2\n        # fail because the first argument is a QuerySet and it is not type Academy\n        self.bc.check.queryset_of(queryset, Academy)  # \ud83d\udd34\n        # fail because the first argument is not a QuerySet\n        self.bc.check.queryset_of(collection, Academy)  # \ud83d\udd34\n        ```\n        \"\"\"\nif not isinstance(query, QuerySet):\nself._parent.fail('The first argument is not a QuerySet')\nif query.model != model:\nself._parent.fail(f'The QuerySet is type {query.model.__name__} instead of {model.__name__}')\ndef queryset_with_pks(self, query: Any, pks: list[int]) -> None:\n\"\"\"\n        Check if the queryset have the following primary keys.\n        Usage:\n        ```py\n        from breathecode.admissions.models import Cohort, Academy\n        self.bc.database.create(cohort=1)\n        collection = []\n        queryset = Cohort.objects.filter()\n        # pass because the QuerySet has the primary keys 1\n        self.bc.check.queryset_with_pks(queryset, [1])  # \ud83d\udfe2\n        # fail because the QuerySet has the primary keys 1 but the second argument is empty\n        self.bc.check.queryset_with_pks(queryset, [])  # \ud83d\udd34\n        ```\n        \"\"\"\nif not isinstance(query, QuerySet):\nself._parent.fail('The first argument is not a QuerySet')\nself._parent.assertEqual([x.pk for x in query], pks)\ndef list_with_pks(self, query: Any, pks: list[int]) -> None:\n\"\"\"\n        Check if the list have the following primary keys.\n        Usage:\n        ```py\n        from breathecode.admissions.models import Cohort, Academy\n        model = self.bc.database.create(cohort=1)\n        collection = [model.cohort]\n        # pass because the QuerySet has the primary keys 1\n        self.bc.check.list_with_pks(collection, [1])  # \ud83d\udfe2\n        # fail because the QuerySet has the primary keys 1 but the second argument is empty\n        self.bc.check.list_with_pks(collection, [])  # \ud83d\udd34\n        ```\n        \"\"\"\nif not isinstance(query, list):\nself._parent.fail('The first argument is not a list')\nself._parent.assertEqual([x.pk for x in query], pks)\n
"},{"location":"testing/mixins/bc-check/#breathecode.tests.mixins.breathecode_mixin.check.Check.calls","title":"calls(first, second)","text":"

Fail if the two objects are partially unequal as determined by the '==' operator.

Usage:

self.bc.check.calls(mock.call_args_list, [call(1, 2, a=3, b=4)])\n
Source code in breathecode/tests/mixins/breathecode_mixin/check.py
def calls(self, first: list[call], second: list[call]) -> None:\n\"\"\"\n    Fail if the two objects are partially unequal as determined by the '==' operator.\n    Usage:\n    ```py\n    self.bc.check.calls(mock.call_args_list, [call(1, 2, a=3, b=4)])\n    ```\n    \"\"\"\n# assert len(first) == len(second), f'not have same length than {first}\\n{second}'\nself._parent.assertEqual(len(first),\nlen(second),\nmsg=f'Does not have same length\\n\\n{first}\\n\\n!=\\n\\n{second}')\nfor i in range(0, len(first)):\nself._parent.assertEqual(first[i].args, second[i].args, msg=f'args in index {i} does not match')\nself._parent.assertEqual(first[i].kwargs,\nsecond[i].kwargs,\nmsg=f'kwargs in index {i} does not match')\n
"},{"location":"testing/mixins/bc-check/#breathecode.tests.mixins.breathecode_mixin.check.Check.datetime_in_range","title":"datetime_in_range(start, end, date)","text":"

Check if a range if between start and end argument.

Usage:

from django.utils import timezone\nstart = timezone.now()\nin_range = timezone.now()\nend = timezone.now()\nout_of_range = timezone.now()\n# pass because this datetime is between start and end\nself.bc.check.datetime_in_range(start, end, in_range)  # \ud83d\udfe2\n# fail because this datetime is not between start and end\nself.bc.check.datetime_in_range(start, end, out_of_range)  # \ud83d\udd34\n
Source code in breathecode/tests/mixins/breathecode_mixin/check.py
def datetime_in_range(self, start: datetime, end: datetime, date: datetime) -> None:\n\"\"\"\n    Check if a range if between start and end argument.\n    Usage:\n    ```py\n    from django.utils import timezone\n    start = timezone.now()\n    in_range = timezone.now()\n    end = timezone.now()\n    out_of_range = timezone.now()\n    # pass because this datetime is between start and end\n    self.bc.check.datetime_in_range(start, end, in_range)  # \ud83d\udfe2\n    # fail because this datetime is not between start and end\n    self.bc.check.datetime_in_range(start, end, out_of_range)  # \ud83d\udd34\n    ```\n    \"\"\"\nself._parent.assertLess(start, date)\nself._parent.assertGreater(end, date)\n
"},{"location":"testing/mixins/bc-check/#breathecode.tests.mixins.breathecode_mixin.check.Check.list_with_pks","title":"list_with_pks(query, pks)","text":"

Check if the list have the following primary keys.

Usage:

from breathecode.admissions.models import Cohort, Academy\nmodel = self.bc.database.create(cohort=1)\ncollection = [model.cohort]\n# pass because the QuerySet has the primary keys 1\nself.bc.check.list_with_pks(collection, [1])  # \ud83d\udfe2\n# fail because the QuerySet has the primary keys 1 but the second argument is empty\nself.bc.check.list_with_pks(collection, [])  # \ud83d\udd34\n
Source code in breathecode/tests/mixins/breathecode_mixin/check.py
def list_with_pks(self, query: Any, pks: list[int]) -> None:\n\"\"\"\n    Check if the list have the following primary keys.\n    Usage:\n    ```py\n    from breathecode.admissions.models import Cohort, Academy\n    model = self.bc.database.create(cohort=1)\n    collection = [model.cohort]\n    # pass because the QuerySet has the primary keys 1\n    self.bc.check.list_with_pks(collection, [1])  # \ud83d\udfe2\n    # fail because the QuerySet has the primary keys 1 but the second argument is empty\n    self.bc.check.list_with_pks(collection, [])  # \ud83d\udd34\n    ```\n    \"\"\"\nif not isinstance(query, list):\nself._parent.fail('The first argument is not a list')\nself._parent.assertEqual([x.pk for x in query], pks)\n
"},{"location":"testing/mixins/bc-check/#breathecode.tests.mixins.breathecode_mixin.check.Check.partial_equality","title":"partial_equality(first, second)","text":"

Fail if the two objects are partially unequal as determined by the '==' operator.

Usage:

obj1 = {'key1': 1, 'key2': 2}\nobj2 = {'key2': 2, 'key3': 1}\nobj3 = {'key2': 2}\n# it's fail because the key3 is not in the obj1\nself.bc.check.partial_equality(obj1, obj2)  # \ud83d\udd34\n# it's fail because the key1 is not in the obj2\nself.bc.check.partial_equality(obj2, obj1)  # \ud83d\udd34\n# it's pass because the key2 exists in the obj1\nself.bc.check.partial_equality(obj1, obj3)  # \ud83d\udfe2\n# it's pass because the key2 exists in the obj2\nself.bc.check.partial_equality(obj2, obj3)  # \ud83d\udfe2\n# it's fail because the key1 is not in the obj3\nself.bc.check.partial_equality(obj3, obj1)  # \ud83d\udd34\n# it's fail because the key3 is not in the obj3\nself.bc.check.partial_equality(obj3, obj2)  # \ud83d\udd34\n
Source code in breathecode/tests/mixins/breathecode_mixin/check.py
def partial_equality(self, first: dict | list[dict], second: dict | list[dict]) -> None:\n\"\"\"\n    Fail if the two objects are partially unequal as determined by the '==' operator.\n    Usage:\n    ```py\n    obj1 = {'key1': 1, 'key2': 2}\n    obj2 = {'key2': 2, 'key3': 1}\n    obj3 = {'key2': 2}\n    # it's fail because the key3 is not in the obj1\n    self.bc.check.partial_equality(obj1, obj2)  # \ud83d\udd34\n    # it's fail because the key1 is not in the obj2\n    self.bc.check.partial_equality(obj2, obj1)  # \ud83d\udd34\n    # it's pass because the key2 exists in the obj1\n    self.bc.check.partial_equality(obj1, obj3)  # \ud83d\udfe2\n    # it's pass because the key2 exists in the obj2\n    self.bc.check.partial_equality(obj2, obj3)  # \ud83d\udfe2\n    # it's fail because the key1 is not in the obj3\n    self.bc.check.partial_equality(obj3, obj1)  # \ud83d\udd34\n    # it's fail because the key3 is not in the obj3\n    self.bc.check.partial_equality(obj3, obj2)  # \ud83d\udd34\n    ```\n    \"\"\"\nassert type(first) == type(second)\nif isinstance(first, list):\nassert len(first) == len(second)\noriginal = []\nfor i in range(0, len(first)):\noriginal.append(self._fill_partial_equality(first[i], second[i]))\nelse:\noriginal = self._fill_partial_equality(first, second)\nself._parent.assertEqual(original, second)\n
"},{"location":"testing/mixins/bc-check/#breathecode.tests.mixins.breathecode_mixin.check.Check.queryset_of","title":"queryset_of(query, model)","text":"

Check if the first argument is a queryset of a models provided as second argument.

Usage:

from breathecode.admissions.models import Cohort, Academy\nself.bc.database.create(cohort=1)\ncollection = []\nqueryset = Cohort.objects.filter()\n# pass because the first argument is a QuerySet and it's type Cohort\nself.bc.check.queryset_of(queryset, Cohort)  # \ud83d\udfe2\n# fail because the first argument is a QuerySet and it is not type Academy\nself.bc.check.queryset_of(queryset, Academy)  # \ud83d\udd34\n# fail because the first argument is not a QuerySet\nself.bc.check.queryset_of(collection, Academy)  # \ud83d\udd34\n
Source code in breathecode/tests/mixins/breathecode_mixin/check.py
def queryset_of(self, query: Any, model: Model) -> None:\n\"\"\"\n    Check if the first argument is a queryset of a models provided as second argument.\n    Usage:\n    ```py\n    from breathecode.admissions.models import Cohort, Academy\n    self.bc.database.create(cohort=1)\n    collection = []\n    queryset = Cohort.objects.filter()\n    # pass because the first argument is a QuerySet and it's type Cohort\n    self.bc.check.queryset_of(queryset, Cohort)  # \ud83d\udfe2\n    # fail because the first argument is a QuerySet and it is not type Academy\n    self.bc.check.queryset_of(queryset, Academy)  # \ud83d\udd34\n    # fail because the first argument is not a QuerySet\n    self.bc.check.queryset_of(collection, Academy)  # \ud83d\udd34\n    ```\n    \"\"\"\nif not isinstance(query, QuerySet):\nself._parent.fail('The first argument is not a QuerySet')\nif query.model != model:\nself._parent.fail(f'The QuerySet is type {query.model.__name__} instead of {model.__name__}')\n
"},{"location":"testing/mixins/bc-check/#breathecode.tests.mixins.breathecode_mixin.check.Check.queryset_with_pks","title":"queryset_with_pks(query, pks)","text":"

Check if the queryset have the following primary keys.

Usage:

from breathecode.admissions.models import Cohort, Academy\nself.bc.database.create(cohort=1)\ncollection = []\nqueryset = Cohort.objects.filter()\n# pass because the QuerySet has the primary keys 1\nself.bc.check.queryset_with_pks(queryset, [1])  # \ud83d\udfe2\n# fail because the QuerySet has the primary keys 1 but the second argument is empty\nself.bc.check.queryset_with_pks(queryset, [])  # \ud83d\udd34\n
Source code in breathecode/tests/mixins/breathecode_mixin/check.py
def queryset_with_pks(self, query: Any, pks: list[int]) -> None:\n\"\"\"\n    Check if the queryset have the following primary keys.\n    Usage:\n    ```py\n    from breathecode.admissions.models import Cohort, Academy\n    self.bc.database.create(cohort=1)\n    collection = []\n    queryset = Cohort.objects.filter()\n    # pass because the QuerySet has the primary keys 1\n    self.bc.check.queryset_with_pks(queryset, [1])  # \ud83d\udfe2\n    # fail because the QuerySet has the primary keys 1 but the second argument is empty\n    self.bc.check.queryset_with_pks(queryset, [])  # \ud83d\udd34\n    ```\n    \"\"\"\nif not isinstance(query, QuerySet):\nself._parent.fail('The first argument is not a QuerySet')\nself._parent.assertEqual([x.pk for x in query], pks)\n
"},{"location":"testing/mixins/bc-database/","title":"bc.database","text":""},{"location":"testing/mixins/bc-database/#breathecode.tests.mixins.breathecode_mixin.database.Database","title":"Database","text":"

Mixin with the purpose of cover all the related with the database

Source code in breathecode/tests/mixins/breathecode_mixin/database.py
class Database:\n\"\"\"Mixin with the purpose of cover all the related with the database\"\"\"\n_cache: dict[str, Model] = {}\n_parent: APITestCase\n_bc: interfaces.BreathecodeInterface\nhow_many = 0\ndef __init__(self, parent, bc: interfaces.BreathecodeInterface) -> None:\nself._parent = parent\nself._bc = bc\ndef reset_queries(self):\nreset_queries()\n# @override_settings(DEBUG=True)\ndef get_queries(self, db='default'):\nreturn [query['sql'] for query in connections[db].queries]\n# @override_settings(DEBUG=True)\ndef print_queries(self, db='default'):\nfor query in connections[db].queries:\nprint(f'{query[\"time\"]} {query[\"sql\"]}\\n')\n@classmethod\ndef get_model(cls, path: str) -> Model:\n\"\"\"\n        Return the model matching the given app_label and model_name.\n        As a shortcut, app_label may be in the form <app_label>.<model_name>.\n        model_name is case-insensitive.\n        Raise LookupError if no application exists with this label, or no\n        model exists with this name in the application. Raise ValueError if\n        called with a single argument that doesn't contain exactly one dot.\n        Usage:\n        ```py\n        # class breathecode.admissions.models.Cohort\n        Cohort = self.bc.database.get_model('admissions.Cohort')\n        ```\n        Keywords arguments:\n        - path(`str`): path to a model, for example `admissions.CohortUser`.\n        \"\"\"\nif path in cls._cache:\nreturn cls._cache[path]\napp_label, model_name = path.split('.')\ncls._cache[path] = apps.get_model(app_label, model_name)\nreturn cls._cache[path]\ndef list_of(self, path: str, dict: bool = True) -> list[Model | dict[str, Any]]:\n\"\"\"\n        This is a wrapper for `Model.objects.filter()`, get a list of values of models as `list[dict]` if\n        `dict=True` else get a list of `Model` instances.\n        Usage:\n        ```py\n        # get all the Cohort as list of dict\n        self.bc.database.get('admissions.Cohort')\n        # get all the Cohort as list of instances of model\n        self.bc.database.get('admissions.Cohort', dict=False)\n        ```\n        Keywords arguments:\n        - path(`str`): path to a model, for example `admissions.CohortUser`.\n        - dict(`bool`): if true return dict of values of model else return model instance.\n        \"\"\"\nmodel = Database.get_model(path)\nresult = model.objects.filter()\nif dict:\nresult = [ModelsMixin.remove_dinamics_fields(self, data.__dict__.copy()) for data in result]\nreturn result\n@database_sync_to_async\ndef async_list_of(self, path: str, dict: bool = True) -> list[Model | dict[str, Any]]:\n\"\"\"\n        This is a wrapper for `Model.objects.filter()`, get a list of values of models as `list[dict]` if\n        `dict=True` else get a list of `Model` instances.\n        Keywords arguments:\n        - path(`str`): path to a model, for example `admissions.CohortUser`.\n        - dict(`bool`): if true return dict of values of model else return model instance.\n        \"\"\"\nreturn self.list_of(path, dict)\ndef delete(self, path: str, pk: Optional[int | str] = None) -> tuple[int, dict[str, int]]:\n\"\"\"\n        This is a wrapper for `Model.objects.filter(pk=pk).delete()`, delete a element if `pk` is provided else\n        all the entries.\n        Usage:\n        ```py\n        # create 19110911 cohorts \ud83e\uddbe\n        self.bc.database.create(cohort=19110911)\n        # exists 19110911 cohorts \ud83e\uddbe\n        self.assertEqual(self.bc.database.count('admissions.Cohort'), 19110911)\n        # remove all the cohorts\n        self.bc.database.delete(10)\n        # exists 19110910 cohorts\n        self.assertEqual(self.bc.database.count('admissions.Cohort'), 19110910)\n        ```\n        # remove all the cohorts\n        self.bc.database.delete()\n        # exists 0 cohorts\n        self.assertEqual(self.bc.database.count('admissions.Cohort'), 0)\n        ```\n        Keywords arguments:\n        - path(`str`): path to a model, for example `admissions.CohortUser`.\n        - pk(`str | int`): primary key of model.\n        \"\"\"\nlookups = {'pk': pk} if pk else {}\nmodel = Database.get_model(path)\nreturn model.objects.filter(**lookups).delete()\ndef get(self, path: str, pk: int or str, dict: bool = True) -> Model | dict[str, Any]:\n\"\"\"\n        This is a wrapper for `Model.objects.filter(pk=pk).first()`, get the values of model as `dict` if\n        `dict=True` else get the `Model` instance.\n        Usage:\n        ```py\n        # get the Cohort with the pk 1 as dict\n        self.bc.database.get('admissions.Cohort', 1)\n        # get the Cohort with the pk 1 as instance of model\n        self.bc.database.get('admissions.Cohort', 1, dict=False)\n        ```\n        Keywords arguments:\n        - path(`str`): path to a model, for example `admissions.CohortUser`.\n        - pk(`str | int`): primary key of model.\n        - dict(`bool`): if true return dict of values of model else return model instance.\n        \"\"\"\nmodel = Database.get_model(path)\nresult = model.objects.filter(pk=pk).first()\nif dict:\nresult = ModelsMixin.remove_dinamics_fields(self, result.__dict__.copy())\nreturn result\n@database_sync_to_async\ndef async_get(self, path: str, pk: int | str, dict: bool = True) -> Model | dict[str, Any]:\n\"\"\"\n        This is a wrapper for `Model.objects.filter(pk=pk).first()`, get the values of model as `dict` if\n        `dict=True` else get the `Model` instance.\n        Keywords arguments:\n        - path(`str`): path to a model, for example `admissions.CohortUser`.\n        - pk(`str | int`): primary key of model.\n        - dict(`bool`): if true return dict of values of model else return model instance.\n        \"\"\"\nreturn self.get(path, pk, dict)\ndef count(self, path: str) -> int:\n\"\"\"\n        This is a wrapper for `Model.objects.count()`, get how many instances of this `Model` are saved.\n        Usage:\n        ```py\n        self.bc.database.count('admissions.Cohort')\n        ```\n        Keywords arguments:\n        - path(`str`): path to a model, for example `admissions.CohortUser`.\n        \"\"\"\nmodel = Database.get_model(path)\nreturn model.objects.count()\n@database_sync_to_async\ndef async_count(self, path: str) -> int:\n\"\"\"\n        This is a wrapper for `Model.objects.count()`, get how many instances of this `Model` are saved.\n        Keywords arguments:\n        - path(`str`): path to a model, for example `admissions.CohortUser`.\n        \"\"\"\nreturn self.count(path)\n@cache\ndef _get_models(self) -> list[Model]:\nvalues = {}\nfor key in apps.app_configs:\nvalues[key] = apps.get_app_config(key).get_models()\nreturn values\ndef camel_case_to_snake_case(self, name):\nname = re.sub('(.)([A-Z][a-z]+)', r'\\1_\\2', name)\nreturn re.sub('([a-z0-9])([A-Z])', r'\\1_\\2', name).lower()\ndef _get_model_field_info(self, model, key):\nattr = getattr(model, key)\nmeta = vars(attr)['field'].related_model._meta\nmodel = vars(attr)['field'].related_model\nblank = attr.field.blank\nnull = attr.field.null\nresult = {\n'field': key,\n'blank': blank,\n'null': null,\n'app_name': meta.app_label,\n'model_name': meta.object_name,\n'handler': attr,\n'model': model,\n}\nif hasattr(attr, 'through'):\nresult['custom_through'] = '_' not in attr.through.__name__\nresult['through_fields'] = attr.rel.through_fields\nreturn result\n@cache\ndef _get_models_descriptors(self) -> list[Model]:\nvalues = {}\napps = self._get_models()\nfor app_key in apps:\nvalues[app_key] = {}\nmodels = apps[app_key]\nfor model in models:\nvalues[app_key][model.__name__] = {}\nvalues[app_key][model.__name__]['meta'] = {\n'app_name': model._meta.app_label,\n'model_name': model._meta.object_name,\n'model': model,\n}\nvalues[app_key][model.__name__]['to_one'] = [\nself._get_model_field_info(model, x) for x in dir(model)\nif isinstance(getattr(model, x), ForwardManyToOneDescriptor)\n]\nvalues[app_key][model.__name__]['to_many'] = [\nself._get_model_field_info(model, x) for x in dir(model)\nif isinstance(getattr(model, x), ManyToManyDescriptor)\n]\nreturn values\n@cache\ndef _get_models_dependencies(self) -> list[Model]:\nvalues = {}\ndescriptors = self._get_models_descriptors()\nfor app_key in descriptors:\nfor descriptor_key in descriptors[app_key]:\ndescriptor = descriptors[app_key][descriptor_key]\nif app_key not in values:\nvalues[app_key] = set()\nprimary_values = values[app_key]['primary'] if 'primary' in values[app_key] else []\nsecondary_values = values[app_key]['secondary'] if 'secondary' in values[app_key] else []\nvalues[app_key] = {\n'primary': {\n*primary_values, *[\nx['app_name']\nfor x in descriptor['to_one'] if x['app_name'] != app_key and x['null'] == False\n], *[\nx['app_name']\nfor x in descriptor['to_many'] if x['app_name'] != app_key and x['null'] == False\n]\n},\n'secondary': {\n*secondary_values, *[\nx['app_name']\nfor x in descriptor['to_one'] if x['app_name'] != app_key and x['null'] == True\n], *[\nx['app_name']\nfor x in descriptor['to_many'] if x['app_name'] != app_key and x['null'] == True\n]\n},\n}\nreturn values\ndef _sort_models_handlers(self,\ndependencies_resolved=None,\nprimary_values=None,\nsecondary_values=None,\nprimary_dependencies=None,\nsecondary_dependencies=None,\nconsume_primary=True) -> list[Model]:\ndependencies_resolved = dependencies_resolved or set()\nprimary_values = primary_values or []\nsecondary_values = secondary_values or []\nif not primary_dependencies and not secondary_dependencies:\ndependencies = self._get_models_dependencies()\nprimary_dependencies = {}\nfor x in dependencies:\nprimary_dependencies[x] = dependencies[x]['primary']\nsecondary_dependencies = {}\nfor x in dependencies:\nsecondary_dependencies[x] = dependencies[x]['secondary']\nfor dependency in dependencies_resolved:\nfor key in primary_dependencies:\nif dependency in primary_dependencies[key]:\nprimary_dependencies[key].remove(dependency)\nprimary_found = [\nx for x in [y for y in primary_dependencies if y not in dependencies_resolved]\nif len(primary_dependencies[x]) == 0\n]\nfor x in primary_found:\ndependencies_resolved.add(x)\nsecondary_found = [\nx for x in [y for y in secondary_dependencies if y not in dependencies_resolved]\nif len(secondary_dependencies[x]) == 0\n]\nif consume_primary and primary_found:\nprimary_values.append(primary_found)\nelif not consume_primary and secondary_found:\nsecondary_values.append(secondary_found)\nfor x in primary_found:\ndel primary_dependencies[x]\nfor dependency in primary_dependencies:\nif x in primary_dependencies[dependency]:\nprimary_dependencies[dependency].remove(x)\nif primary_dependencies:\nreturn self._sort_models_handlers(dependencies_resolved,\nprimary_values,\nsecondary_values,\nprimary_dependencies,\nsecondary_dependencies,\nconsume_primary=True)\nif secondary_dependencies:\nreturn primary_values, [x for x in secondary_dependencies if len(secondary_dependencies[x])]\nreturn primary_values, secondary_values\n@cache\ndef _get_models_handlers(self) -> list[Model]:\narguments = {}\narguments_banned = set()\norder, deferred = self._sort_models_handlers()\ndescriptors = self._get_models_descriptors()\ndef manage_model(models, descriptor, *args, **kwargs):\nmodel_field_name = self.camel_case_to_snake_case(descriptor['meta']['model_name'])\napp_name = descriptor['meta']['app_name']\nmodel_name = descriptor['meta']['model_name']\nif model_field_name in kwargs and f'{app_name}__{model_field_name}' in kwargs:\nraise Exception(f'Exists many apps with the same model name `{model_name}`, please use '\nf'`{app_name}__{model_field_name}` instead of `{model_field_name}`')\narg = False\nif f'{app_name}__{model_field_name}' in kwargs:\narg = kwargs[f'{app_name}__{model_field_name}']\nelif model_field_name in kwargs:\narg = kwargs[model_field_name]\nif not model_field_name in models and is_valid(arg):\nkargs = {}\nfor x in descriptor['to_one']:\nrelated_model_field_name = self.camel_case_to_snake_case(x['model_name'])\nif related_model_field_name in models:\nkargs[x['field']] = just_one(models[related_model_field_name])\nwithout_through = [x for x in descriptor['to_many'] if x['custom_through'] == False]\nfor x in without_through:\nrelated_model_field_name = self.camel_case_to_snake_case(x['model_name'])\nif related_model_field_name in models:\nkargs[x['field']] = get_list(models[related_model_field_name])\nmodels[model_field_name] = create_models(arg, f'{app_name}.{model_name}', **kargs)\nwith_through = [\nx for x in descriptor['to_many']\nif x['custom_through'] == True and not x['field'].endswith('_set')\n]\nfor x in with_through:\nrelated_model_field_name = self.camel_case_to_snake_case(x['model_name'])\nif related_model_field_name in models:\nfor item in get_list(models[related_model_field_name]):\nthrough_current = x['through_fields'][0]\nthrough_related = x['through_fields'][1]\nthrough_args = {through_current: models[model_field_name], through_related: item}\nx['handler'].through.objects.create(**through_args)\nreturn models\ndef link_deferred_model(models, descriptor, *args, **kwargs):\nmodel_field_name = self.camel_case_to_snake_case(descriptor['meta']['model_name'])\napp_name = descriptor['meta']['app_name']\nmodel_name = descriptor['meta']['model_name']\nif model_field_name in kwargs and f'{app_name}__{model_field_name}' in kwargs:\nraise Exception(f'Exists many apps with the same model name `{model_name}`, please use '\nf'`{app_name}__{model_field_name}` instead of `{model_field_name}`')\nif model_field_name in models:\nitems = models[model_field_name] if isinstance(models[model_field_name],\nlist) else [models[model_field_name]]\nfor m in items:\nfor x in descriptor['to_one']:\nrelated_model_field_name = self.camel_case_to_snake_case(x['model_name'])\nmodel_exists = related_model_field_name in models\nis_list = isinstance(models[model_field_name], list) if model_exists else False\nif model_exists and not is_list and not getattr(models[model_field_name], x['field']):\nsetattr(m, x['field'], just_one(models[related_model_field_name]))\nif model_exists and is_list:\nfor y in models[model_field_name]:\nif getattr(y, x['field']):\nsetattr(m, x['field'], just_one(models[related_model_field_name]))\nfor x in descriptor['to_many']:\nrelated_model_field_name = self.camel_case_to_snake_case(x['model_name'])\nif related_model_field_name in models and not getattr(\nmodels[model_field_name], x['field']):\nsetattr(m, x['field'], get_list(models[related_model_field_name]))\nsetattr(m, '__mixer__', None)\nm.save()\nreturn models\ndef wrapper(*args, **kwargs):\nmodels = {}\nfor generation_round in order:\nfor app_key in generation_round:\nfor descriptor_key in descriptors[app_key]:\ndescriptor = descriptors[app_key][descriptor_key]\nattr = self.camel_case_to_snake_case(descriptor['meta']['model_name'])\nmodels = manage_model(models, descriptor, *args, **kwargs)\nif app_key not in arguments:\narguments[app_key] = {}\narguments[attr] = ...\nelse:\narguments_banned.add(attr)\narguments[f'{app_key}__{attr}'] = ...\nfor generation_round in order:\nfor app_key in generation_round:\nfor descriptor_key in descriptors[app_key]:\ndescriptor = descriptors[app_key][descriptor_key]\nattr = self.camel_case_to_snake_case(descriptor['meta']['model_name'])\nmodels = link_deferred_model(models, descriptor, *args, **kwargs)\nif app_key not in arguments:\narguments[app_key] = {}\narguments[attr] = ...\nelse:\narguments_banned.add(attr)\narguments[f'{app_key}__{attr}'] = ...\nreturn AttrDict(**models)\nreturn wrapper\ndef create_v2(self, *args, **kwargs) -> dict[str, Model | list[Model]]:\n\"\"\"\n        Unstable version of mixin that create all models, do not use this.\n        \"\"\"\nmodels = self._get_models_handlers()(*args, **kwargs)\nreturn models\ndef create(self, *args, **kwargs) -> dict[str, Model | list[Model]]:\n\"\"\"\n        Create one o many instances of models and return it like a dict of models.\n        Usage:\n        ```py\n        # create three users\n        self.bc.database.create(user=3)\n        # create one user with a specific first name\n        user = {'first_name': 'Lacey'}\n        self.bc.database.create(user=user)\n        # create two users with a specific first name and last name\n        users = [\n            {'first_name': 'Lacey', 'last_name': 'Sturm'},\n            {'first_name': 'The', 'last_name': 'Warning'},\n        ]\n        self.bc.database.create(user=users)\n        # create two users with the same first name\n        user = {'first_name': 'Lacey'}\n        self.bc.database.create(user=(2, user))\n        # setting up manually the relationships\n        cohort_user = {'cohort_id': 2}\n        self.bc.database.create(cohort=2, cohort_user=cohort_user)\n        ```\n        It get the model name as snake case, you can pass a `bool`, `int`, `dict`, `tuple`, `list[dict]` or\n        `list[tuple]`.\n        Behavior for type of argument:\n        - `bool`: if it is true generate a instance of a model.\n        - `int`: generate a instance of a model n times, if `n` > 1 this is a list.\n        - `dict`: generate a instance of a model, this pass to mixer.blend custom values to the model.\n        - `tuple`: one element need to be a int and the other be a dict, generate a instance of a model n times,\n        if `n` > 1 this is a list, this pass to mixer.blend custom values to the model.\n        - `list[dict]`: generate a instance of a model n times, if `n` > 1 this is a list,\n        this pass to mixer.blend custom values to the model.\n        - `list[tuple]`: generate a instance of a model n times, if `n` > 1 this is a list for each element,\n        this pass to mixer.blend custom values to the model.\n        Keywords arguments deprecated:\n        - models: this arguments is use to implement inheritance, receive as argument the output of other\n        `self.bc.database.create()` execution.\n        - authenticate: create a user and use `APITestCase.client.force_authenticate(user=models['user'])` to\n        get credentials.\n        \"\"\"\nreturn GenerateModelsMixin.generate_models(self._parent, _new_implementation=True, *args, **kwargs)\n@database_sync_to_async\ndef async_create(self, *args, **kwargs) -> dict[str, Model | list[Model]]:\n\"\"\"\n        This is a wrapper for `Model.objects.count()`, get how many instances of this `Model` are saved.\n        Keywords arguments:\n        - path(`str`): path to a model, for example `admissions.CohortUser`.\n        \"\"\"\nreturn self.create(*args, **kwargs)\n
"},{"location":"testing/mixins/bc-database/#breathecode.tests.mixins.breathecode_mixin.database.Database.async_count","title":"async_count(path)","text":"

This is a wrapper for Model.objects.count(), get how many instances of this Model are saved.

Keywords arguments: - path(str): path to a model, for example admissions.CohortUser.

Source code in breathecode/tests/mixins/breathecode_mixin/database.py
@database_sync_to_async\ndef async_count(self, path: str) -> int:\n\"\"\"\n    This is a wrapper for `Model.objects.count()`, get how many instances of this `Model` are saved.\n    Keywords arguments:\n    - path(`str`): path to a model, for example `admissions.CohortUser`.\n    \"\"\"\nreturn self.count(path)\n
"},{"location":"testing/mixins/bc-database/#breathecode.tests.mixins.breathecode_mixin.database.Database.async_create","title":"async_create(*args, **kwargs)","text":"

This is a wrapper for Model.objects.count(), get how many instances of this Model are saved.

Keywords arguments: - path(str): path to a model, for example admissions.CohortUser.

Source code in breathecode/tests/mixins/breathecode_mixin/database.py
@database_sync_to_async\ndef async_create(self, *args, **kwargs) -> dict[str, Model | list[Model]]:\n\"\"\"\n    This is a wrapper for `Model.objects.count()`, get how many instances of this `Model` are saved.\n    Keywords arguments:\n    - path(`str`): path to a model, for example `admissions.CohortUser`.\n    \"\"\"\nreturn self.create(*args, **kwargs)\n
"},{"location":"testing/mixins/bc-database/#breathecode.tests.mixins.breathecode_mixin.database.Database.async_get","title":"async_get(path, pk, dict=True)","text":"

This is a wrapper for Model.objects.filter(pk=pk).first(), get the values of model as dict if dict=True else get the Model instance.

Keywords arguments: - path(str): path to a model, for example admissions.CohortUser. - pk(str | int): primary key of model. - dict(bool): if true return dict of values of model else return model instance.

Source code in breathecode/tests/mixins/breathecode_mixin/database.py
@database_sync_to_async\ndef async_get(self, path: str, pk: int | str, dict: bool = True) -> Model | dict[str, Any]:\n\"\"\"\n    This is a wrapper for `Model.objects.filter(pk=pk).first()`, get the values of model as `dict` if\n    `dict=True` else get the `Model` instance.\n    Keywords arguments:\n    - path(`str`): path to a model, for example `admissions.CohortUser`.\n    - pk(`str | int`): primary key of model.\n    - dict(`bool`): if true return dict of values of model else return model instance.\n    \"\"\"\nreturn self.get(path, pk, dict)\n
"},{"location":"testing/mixins/bc-database/#breathecode.tests.mixins.breathecode_mixin.database.Database.async_list_of","title":"async_list_of(path, dict=True)","text":"

This is a wrapper for Model.objects.filter(), get a list of values of models as list[dict] if dict=True else get a list of Model instances.

Keywords arguments: - path(str): path to a model, for example admissions.CohortUser. - dict(bool): if true return dict of values of model else return model instance.

Source code in breathecode/tests/mixins/breathecode_mixin/database.py
@database_sync_to_async\ndef async_list_of(self, path: str, dict: bool = True) -> list[Model | dict[str, Any]]:\n\"\"\"\n    This is a wrapper for `Model.objects.filter()`, get a list of values of models as `list[dict]` if\n    `dict=True` else get a list of `Model` instances.\n    Keywords arguments:\n    - path(`str`): path to a model, for example `admissions.CohortUser`.\n    - dict(`bool`): if true return dict of values of model else return model instance.\n    \"\"\"\nreturn self.list_of(path, dict)\n
"},{"location":"testing/mixins/bc-database/#breathecode.tests.mixins.breathecode_mixin.database.Database.count","title":"count(path)","text":"

This is a wrapper for Model.objects.count(), get how many instances of this Model are saved.

Usage:

self.bc.database.count('admissions.Cohort')\n

Keywords arguments: - path(str): path to a model, for example admissions.CohortUser.

Source code in breathecode/tests/mixins/breathecode_mixin/database.py
def count(self, path: str) -> int:\n\"\"\"\n    This is a wrapper for `Model.objects.count()`, get how many instances of this `Model` are saved.\n    Usage:\n    ```py\n    self.bc.database.count('admissions.Cohort')\n    ```\n    Keywords arguments:\n    - path(`str`): path to a model, for example `admissions.CohortUser`.\n    \"\"\"\nmodel = Database.get_model(path)\nreturn model.objects.count()\n
"},{"location":"testing/mixins/bc-database/#breathecode.tests.mixins.breathecode_mixin.database.Database.create","title":"create(*args, **kwargs)","text":"

Create one o many instances of models and return it like a dict of models.

Usage:

# create three users\nself.bc.database.create(user=3)\n# create one user with a specific first name\nuser = {'first_name': 'Lacey'}\nself.bc.database.create(user=user)\n# create two users with a specific first name and last name\nusers = [\n{'first_name': 'Lacey', 'last_name': 'Sturm'},\n{'first_name': 'The', 'last_name': 'Warning'},\n]\nself.bc.database.create(user=users)\n# create two users with the same first name\nuser = {'first_name': 'Lacey'}\nself.bc.database.create(user=(2, user))\n# setting up manually the relationships\ncohort_user = {'cohort_id': 2}\nself.bc.database.create(cohort=2, cohort_user=cohort_user)\n

It get the model name as snake case, you can pass a bool, int, dict, tuple, list[dict] or list[tuple].

Behavior for type of argument:

  • bool: if it is true generate a instance of a model.
  • int: generate a instance of a model n times, if n > 1 this is a list.
  • dict: generate a instance of a model, this pass to mixer.blend custom values to the model.
  • tuple: one element need to be a int and the other be a dict, generate a instance of a model n times, if n > 1 this is a list, this pass to mixer.blend custom values to the model.
  • list[dict]: generate a instance of a model n times, if n > 1 this is a list, this pass to mixer.blend custom values to the model.
  • list[tuple]: generate a instance of a model n times, if n > 1 this is a list for each element, this pass to mixer.blend custom values to the model.

Keywords arguments deprecated: - models: this arguments is use to implement inheritance, receive as argument the output of other self.bc.database.create() execution. - authenticate: create a user and use APITestCase.client.force_authenticate(user=models['user']) to get credentials.

Source code in breathecode/tests/mixins/breathecode_mixin/database.py
def create(self, *args, **kwargs) -> dict[str, Model | list[Model]]:\n\"\"\"\n    Create one o many instances of models and return it like a dict of models.\n    Usage:\n    ```py\n    # create three users\n    self.bc.database.create(user=3)\n    # create one user with a specific first name\n    user = {'first_name': 'Lacey'}\n    self.bc.database.create(user=user)\n    # create two users with a specific first name and last name\n    users = [\n        {'first_name': 'Lacey', 'last_name': 'Sturm'},\n        {'first_name': 'The', 'last_name': 'Warning'},\n    ]\n    self.bc.database.create(user=users)\n    # create two users with the same first name\n    user = {'first_name': 'Lacey'}\n    self.bc.database.create(user=(2, user))\n    # setting up manually the relationships\n    cohort_user = {'cohort_id': 2}\n    self.bc.database.create(cohort=2, cohort_user=cohort_user)\n    ```\n    It get the model name as snake case, you can pass a `bool`, `int`, `dict`, `tuple`, `list[dict]` or\n    `list[tuple]`.\n    Behavior for type of argument:\n    - `bool`: if it is true generate a instance of a model.\n    - `int`: generate a instance of a model n times, if `n` > 1 this is a list.\n    - `dict`: generate a instance of a model, this pass to mixer.blend custom values to the model.\n    - `tuple`: one element need to be a int and the other be a dict, generate a instance of a model n times,\n    if `n` > 1 this is a list, this pass to mixer.blend custom values to the model.\n    - `list[dict]`: generate a instance of a model n times, if `n` > 1 this is a list,\n    this pass to mixer.blend custom values to the model.\n    - `list[tuple]`: generate a instance of a model n times, if `n` > 1 this is a list for each element,\n    this pass to mixer.blend custom values to the model.\n    Keywords arguments deprecated:\n    - models: this arguments is use to implement inheritance, receive as argument the output of other\n    `self.bc.database.create()` execution.\n    - authenticate: create a user and use `APITestCase.client.force_authenticate(user=models['user'])` to\n    get credentials.\n    \"\"\"\nreturn GenerateModelsMixin.generate_models(self._parent, _new_implementation=True, *args, **kwargs)\n
"},{"location":"testing/mixins/bc-database/#breathecode.tests.mixins.breathecode_mixin.database.Database.create_v2","title":"create_v2(*args, **kwargs)","text":"

Unstable version of mixin that create all models, do not use this.

Source code in breathecode/tests/mixins/breathecode_mixin/database.py
def create_v2(self, *args, **kwargs) -> dict[str, Model | list[Model]]:\n\"\"\"\n    Unstable version of mixin that create all models, do not use this.\n    \"\"\"\nmodels = self._get_models_handlers()(*args, **kwargs)\nreturn models\n
"},{"location":"testing/mixins/bc-database/#breathecode.tests.mixins.breathecode_mixin.database.Database.delete","title":"delete(path, pk=None)","text":"

This is a wrapper for Model.objects.filter(pk=pk).delete(), delete a element if pk is provided else all the entries.

Usage:

# create 19110911 cohorts \ud83e\uddbe\nself.bc.database.create(cohort=19110911)\n# exists 19110911 cohorts \ud83e\uddbe\nself.assertEqual(self.bc.database.count('admissions.Cohort'), 19110911)\n# remove all the cohorts\nself.bc.database.delete(10)\n# exists 19110910 cohorts\nself.assertEqual(self.bc.database.count('admissions.Cohort'), 19110910)\n
"},{"location":"testing/mixins/bc-database/#breathecode.tests.mixins.breathecode_mixin.database.Database.delete--remove-all-the-cohorts","title":"remove all the cohorts","text":"

self.bc.database.delete()

"},{"location":"testing/mixins/bc-database/#breathecode.tests.mixins.breathecode_mixin.database.Database.delete--exists-0-cohorts","title":"exists 0 cohorts","text":"

self.assertEqual(self.bc.database.count('admissions.Cohort'), 0) ```

Keywords arguments: - path(str): path to a model, for example admissions.CohortUser. - pk(str | int): primary key of model.

Source code in breathecode/tests/mixins/breathecode_mixin/database.py
def delete(self, path: str, pk: Optional[int | str] = None) -> tuple[int, dict[str, int]]:\n\"\"\"\n    This is a wrapper for `Model.objects.filter(pk=pk).delete()`, delete a element if `pk` is provided else\n    all the entries.\n    Usage:\n    ```py\n    # create 19110911 cohorts \ud83e\uddbe\n    self.bc.database.create(cohort=19110911)\n    # exists 19110911 cohorts \ud83e\uddbe\n    self.assertEqual(self.bc.database.count('admissions.Cohort'), 19110911)\n    # remove all the cohorts\n    self.bc.database.delete(10)\n    # exists 19110910 cohorts\n    self.assertEqual(self.bc.database.count('admissions.Cohort'), 19110910)\n    ```\n    # remove all the cohorts\n    self.bc.database.delete()\n    # exists 0 cohorts\n    self.assertEqual(self.bc.database.count('admissions.Cohort'), 0)\n    ```\n    Keywords arguments:\n    - path(`str`): path to a model, for example `admissions.CohortUser`.\n    - pk(`str | int`): primary key of model.\n    \"\"\"\nlookups = {'pk': pk} if pk else {}\nmodel = Database.get_model(path)\nreturn model.objects.filter(**lookups).delete()\n
"},{"location":"testing/mixins/bc-database/#breathecode.tests.mixins.breathecode_mixin.database.Database.get","title":"get(path, pk, dict=True)","text":"

This is a wrapper for Model.objects.filter(pk=pk).first(), get the values of model as dict if dict=True else get the Model instance.

Usage:

# get the Cohort with the pk 1 as dict\nself.bc.database.get('admissions.Cohort', 1)\n# get the Cohort with the pk 1 as instance of model\nself.bc.database.get('admissions.Cohort', 1, dict=False)\n

Keywords arguments: - path(str): path to a model, for example admissions.CohortUser. - pk(str | int): primary key of model. - dict(bool): if true return dict of values of model else return model instance.

Source code in breathecode/tests/mixins/breathecode_mixin/database.py
def get(self, path: str, pk: int or str, dict: bool = True) -> Model | dict[str, Any]:\n\"\"\"\n    This is a wrapper for `Model.objects.filter(pk=pk).first()`, get the values of model as `dict` if\n    `dict=True` else get the `Model` instance.\n    Usage:\n    ```py\n    # get the Cohort with the pk 1 as dict\n    self.bc.database.get('admissions.Cohort', 1)\n    # get the Cohort with the pk 1 as instance of model\n    self.bc.database.get('admissions.Cohort', 1, dict=False)\n    ```\n    Keywords arguments:\n    - path(`str`): path to a model, for example `admissions.CohortUser`.\n    - pk(`str | int`): primary key of model.\n    - dict(`bool`): if true return dict of values of model else return model instance.\n    \"\"\"\nmodel = Database.get_model(path)\nresult = model.objects.filter(pk=pk).first()\nif dict:\nresult = ModelsMixin.remove_dinamics_fields(self, result.__dict__.copy())\nreturn result\n
"},{"location":"testing/mixins/bc-database/#breathecode.tests.mixins.breathecode_mixin.database.Database.get_model","title":"get_model(path) classmethod","text":"

Return the model matching the given app_label and model_name.

As a shortcut, app_label may be in the form ..

model_name is case-insensitive.

Raise LookupError if no application exists with this label, or no model exists with this name in the application. Raise ValueError if called with a single argument that doesn't contain exactly one dot.

Usage:

# class breathecode.admissions.models.Cohort\nCohort = self.bc.database.get_model('admissions.Cohort')\n

Keywords arguments: - path(str): path to a model, for example admissions.CohortUser.

Source code in breathecode/tests/mixins/breathecode_mixin/database.py
@classmethod\ndef get_model(cls, path: str) -> Model:\n\"\"\"\n    Return the model matching the given app_label and model_name.\n    As a shortcut, app_label may be in the form <app_label>.<model_name>.\n    model_name is case-insensitive.\n    Raise LookupError if no application exists with this label, or no\n    model exists with this name in the application. Raise ValueError if\n    called with a single argument that doesn't contain exactly one dot.\n    Usage:\n    ```py\n    # class breathecode.admissions.models.Cohort\n    Cohort = self.bc.database.get_model('admissions.Cohort')\n    ```\n    Keywords arguments:\n    - path(`str`): path to a model, for example `admissions.CohortUser`.\n    \"\"\"\nif path in cls._cache:\nreturn cls._cache[path]\napp_label, model_name = path.split('.')\ncls._cache[path] = apps.get_model(app_label, model_name)\nreturn cls._cache[path]\n
"},{"location":"testing/mixins/bc-database/#breathecode.tests.mixins.breathecode_mixin.database.Database.list_of","title":"list_of(path, dict=True)","text":"

This is a wrapper for Model.objects.filter(), get a list of values of models as list[dict] if dict=True else get a list of Model instances.

Usage:

# get all the Cohort as list of dict\nself.bc.database.get('admissions.Cohort')\n# get all the Cohort as list of instances of model\nself.bc.database.get('admissions.Cohort', dict=False)\n

Keywords arguments: - path(str): path to a model, for example admissions.CohortUser. - dict(bool): if true return dict of values of model else return model instance.

Source code in breathecode/tests/mixins/breathecode_mixin/database.py
def list_of(self, path: str, dict: bool = True) -> list[Model | dict[str, Any]]:\n\"\"\"\n    This is a wrapper for `Model.objects.filter()`, get a list of values of models as `list[dict]` if\n    `dict=True` else get a list of `Model` instances.\n    Usage:\n    ```py\n    # get all the Cohort as list of dict\n    self.bc.database.get('admissions.Cohort')\n    # get all the Cohort as list of instances of model\n    self.bc.database.get('admissions.Cohort', dict=False)\n    ```\n    Keywords arguments:\n    - path(`str`): path to a model, for example `admissions.CohortUser`.\n    - dict(`bool`): if true return dict of values of model else return model instance.\n    \"\"\"\nmodel = Database.get_model(path)\nresult = model.objects.filter()\nif dict:\nresult = [ModelsMixin.remove_dinamics_fields(self, data.__dict__.copy()) for data in result]\nreturn result\n
"},{"location":"testing/mixins/bc-datetime/","title":"bc.datetime","text":""},{"location":"testing/mixins/bc-datetime/#breathecode.tests.mixins.breathecode_mixin.datetime.Datetime","title":"Datetime","text":"

Mixin with the purpose of cover all the related with datetime

Source code in breathecode/tests/mixins/breathecode_mixin/datetime.py
class Datetime:\n\"\"\"Mixin with the purpose of cover all the related with datetime\"\"\"\nto_iso_string = DatetimeMixin.datetime_to_iso\nfrom_iso_string = DatetimeMixin.iso_to_datetime\nnow = DatetimeMixin.datetime_now\n_parent: APITestCase\n_bc: interfaces.BreathecodeInterface\ndef __init__(self, parent, bc: interfaces.BreathecodeInterface) -> None:\nself._parent = parent\nself._bc = bc\ndef from_timedelta(self, delta=timedelta(seconds=0)) -> str:\n\"\"\"\n        Transform from timedelta to the totals seconds in str.\n        Usage:\n        ```py\n        from datetime import timedelta\n        delta = timedelta(seconds=777)\n        self.bc.datetime.from_timedelta(delta)  # equals to '777.0'\n        ```\n        \"\"\"\nreturn str(delta.total_seconds())\ndef to_datetime_integer(self, timezone: str, date: datetime) -> int:\n\"\"\"\n        Transform datetime to datetime integer.\n        Usage:\n        ```py\n        utc_now = timezone.now()\n        # date\n        date = datetime.datetime(2022, 3, 21, 2, 51, 55, 068)\n        # equals to 202203210751\n        self.bc.datetime.to_datetime_integer('america/new_york', date)\n        ```\n        \"\"\"\nreturn DatetimeInteger.from_datetime(timezone, date)\n
"},{"location":"testing/mixins/bc-datetime/#breathecode.tests.mixins.breathecode_mixin.datetime.Datetime.from_timedelta","title":"from_timedelta(delta=timedelta(seconds=0))","text":"

Transform from timedelta to the totals seconds in str.

Usage:

from datetime import timedelta\ndelta = timedelta(seconds=777)\nself.bc.datetime.from_timedelta(delta)  # equals to '777.0'\n
Source code in breathecode/tests/mixins/breathecode_mixin/datetime.py
def from_timedelta(self, delta=timedelta(seconds=0)) -> str:\n\"\"\"\n    Transform from timedelta to the totals seconds in str.\n    Usage:\n    ```py\n    from datetime import timedelta\n    delta = timedelta(seconds=777)\n    self.bc.datetime.from_timedelta(delta)  # equals to '777.0'\n    ```\n    \"\"\"\nreturn str(delta.total_seconds())\n
"},{"location":"testing/mixins/bc-datetime/#breathecode.tests.mixins.breathecode_mixin.datetime.Datetime.to_datetime_integer","title":"to_datetime_integer(timezone, date)","text":"

Transform datetime to datetime integer.

Usage:

utc_now = timezone.now()\n# date\ndate = datetime.datetime(2022, 3, 21, 2, 51, 55, 068)\n# equals to 202203210751\nself.bc.datetime.to_datetime_integer('america/new_york', date)\n
Source code in breathecode/tests/mixins/breathecode_mixin/datetime.py
def to_datetime_integer(self, timezone: str, date: datetime) -> int:\n\"\"\"\n    Transform datetime to datetime integer.\n    Usage:\n    ```py\n    utc_now = timezone.now()\n    # date\n    date = datetime.datetime(2022, 3, 21, 2, 51, 55, 068)\n    # equals to 202203210751\n    self.bc.datetime.to_datetime_integer('america/new_york', date)\n    ```\n    \"\"\"\nreturn DatetimeInteger.from_datetime(timezone, date)\n
"},{"location":"testing/mixins/bc-fake/","title":"bc.fake","text":"

Represents a instance of Faker you can learn about it in their webside

"},{"location":"testing/mixins/bc-format/","title":"bc.format","text":""},{"location":"testing/mixins/bc-format/#breathecode.tests.mixins.breathecode_mixin.format.Format","title":"Format","text":"

Mixin with the purpose of cover all the related with format or parse something

Source code in breathecode/tests/mixins/breathecode_mixin/format.py
class Format:\n\"\"\"Mixin with the purpose of cover all the related with format or parse something\"\"\"\n_parent: APITestCase\n_bc: interfaces.BreathecodeInterface\nENCODE = ENCODE\ndef __init__(self, parent, bc: interfaces.BreathecodeInterface) -> None:\nself._parent = parent\nself._bc = bc\ndef call(self, *args: Any, **kwargs: Any) -> str:\n\"\"\"\n        Wraps a call into it and return its args and kwargs.\n        example:\n        ```py\n        args, kwargs = self.bc.format.call(2, 3, 4, a=1, b=2, c=3)\n        assert args == (2, 3, 4)\n        assert kwargs == {'a': 1, 'b': 2, 'c': 3}\n        ```\n        \"\"\"\nreturn args, kwargs\ndef querystring(self, query: dict) -> str:\n\"\"\"\n        Build a querystring from a given dict.\n        \"\"\"\nreturn urllib.parse.urlencode(query)\ndef queryset(self, query: dict) -> str:\n\"\"\"\n        Build a QuerySet from a given dict.\n        \"\"\"\nreturn Q(**query)\n# remove lang from args\ndef lookup(self, lang: str, overwrite: dict = dict(), **kwargs: dict | tuple) -> dict[str, Any]:\n\"\"\"\n        Generate from lookups the values in test side to be used in querystring.\n        example:\n        ```py\n        query = self.bc.format.lookup(\n            'en',\n            strings={\n                'exact': [\n                    'remote_meeting_url',\n                ],\n            },\n            bools={\n                'is_null': ['ended_at'],\n            },\n            datetimes={\n                'gte': ['starting_at'],\n                'lte': ['ending_at'],\n            },\n            slugs=[\n                'cohort_time_slot__cohort',\n                'cohort_time_slot__cohort__academy',\n                'cohort_time_slot__cohort__syllabus_version__syllabus',\n            ],\n            overwrite={\n                'cohort': 'cohort_time_slot__cohort',\n                'academy': 'cohort_time_slot__cohort__academy',\n                'syllabus': 'cohort_time_slot__cohort__syllabus_version__syllabus',\n                'start': 'starting_at',\n                'end': 'ending_at',\n                'upcoming': 'ended_at',\n            },\n        )\n        url = reverse_lazy('events:me_event_liveclass') + '?' + self.bc.format.querystring(query)\n        # this test avoid to pass a invalid param to ORM\n        response = self.client.get(url)\n        ```\n        \"\"\"\nresult = {}\n# foreign\nids = kwargs.get('ids', tuple())\nslugs = kwargs.get('slugs', tuple())\n# fields\nints = kwargs.get('ints', dict())\nstrings = kwargs.get('strings', dict())\ndatetimes = kwargs.get('datetimes', dict())\nbools = kwargs.get('bools', dict())\n# opts\ncustom_fields = kwargs.get('custom_fields', dict())\n# serialize foreign\nids = tuple(ids)\nslugs = tuple(slugs)\noverwrite = dict([(v, k) for k, v in overwrite.items()])\n# foreign\nfor field in ids:\nif field == '':\nresult['id'] = field.integer('exact')\ncontinue\nname = overwrite.get(field, field)\nresult[name] = Field.id('')\nfor field in slugs:\nif field == '':\nresult['id'] = Field.integer('exact')\nresult['slug'] = Field.string('exact')\ncontinue\nname = overwrite.get(field, field)\nresult[name] = Field.slug('')\n# fields\nfor mode in ints:\nfor field in ints[mode]:\nname = overwrite.get(field, field)\nresult[name] = Field.int(mode)\nfor mode in strings:\nfor field in strings[mode]:\nname = overwrite.get(field, field)\nresult[name] = Field.string(mode)\nfor mode in datetimes:\nfor field in datetimes[mode]:\nname = overwrite.get(field, field)\nresult[name] = Field.datetime(mode)\nfor mode in bools:\nfor field in bools[mode]:\nname = overwrite.get(field, field)\nresult[name] = Field.bool(mode)\n# custom fields\nfor field in custom_fields:\nname = overwrite.get(field, field)\nresult[name] = custom_fields[field]()\nreturn result\ndef table(self, arg: QuerySet) -> dict[str, Any] | list[dict[str, Any]]:\n\"\"\"\n        Convert a QuerySet in a list.\n        Usage:\n        ```py\n        model = self.bc.database.create(user=1, group=1)\n        self.bc.format.model(model.user.groups.all())  # = [{...}]\n        ```\n        \"\"\"\nreturn [ModelsMixin.remove_dinamics_fields(self, data.__dict__.copy()) for data in arg]\ndef to_dict(self, arg: Any) -> dict[str, Any] | list[dict[str, Any]]:\n\"\"\"\n        Parse the object to a `dict` or `list[dict]`.\n        Usage:\n        ```py\n        # setup the database, model.user is instance of dict and model.cohort\n        # is instance list of dicts\n        model = self.bc.database.create(user=1, cohort=2)\n        # Parsing one model to a dict\n        self.bc.format.to_dict(model.user)  # = {...}\n        # Parsing many models to a list of dict (infered from the type of\n        # argument)\n        self.bc.format.to_dict(model.cohort)  # = [{...}, {...}]\n        ```\n        \"\"\"\nif isinstance(arg, list) or isinstance(arg, QuerySet):\nreturn [self._one_to_dict(x) for x in arg]\nreturn self._one_to_dict(arg)\ndef to_decimal_string(self, decimal: int | float) -> str:\n\"\"\"\n        Parse a number to the django representation of a decimal.\n        Usage:\n        ```py\n        self.bc.format.to_decimal(1)  # returns '1.000000000000000'\n        ```\n        \"\"\"\nreturn '%.15f' % round(decimal, 15)\ndef _one_to_dict(self, arg) -> dict[str, Any]:\n\"\"\"Parse the object to a `dict`\"\"\"\nif isinstance(arg, Model):\nreturn ModelsMixin.remove_dinamics_fields(None, vars(arg))\nif isinstance(arg, dict):\nreturn arg\nraise NotImplementedError(f'{arg.__name__} is not implemented yet')\ndef describe_models(self, models: dict[str, Model]) -> str:\n\"\"\"\n        Describe the models.\n        Usage:\n        ```py\n        # setup the database\n        model = self.bc.database.create(user=1, cohort=1)\n        # print the docstring to the corresponding test\n        self.bc.format.describe_models(model)\n        ```\n        \"\"\"\ntitle_spaces = ' ' * 8\nmodel_spaces = ' ' * 10\nresult = {}\nfor key in models:\nmodel = models[key]\nif isinstance(model, list):\nfor v in model:\nname, obj = self._describe_model(v)\nresult[name] = obj\nelse:\nname, obj = self._describe_model(model)\nresult[name] = obj\nprint(title_spaces + 'Descriptions of models are being generated:')\nfor line in yaml.dump(result).split('\\n'):\nif not line.startswith(' '):\nprint()\nprint(model_spaces + line)\n# This make sure the element are being printed and prevent `describe_models` are pushed to dev branch\nassert False\n#TODO: this method is buggy in the line `if not hasattr(model, key)`\ndef _describe_model(self, model: Model):\npk_name = self._get_pk_name(model)\nattrs = dir(model)\nresult = {}\nfor key in attrs:\nif key.startswith('_'):\ncontinue\nif key == 'DoesNotExist':\ncontinue\nif key == 'MultipleObjectsReturned':\ncontinue\nif key.startswith('get_next_'):\ncontinue\nif key.startswith('get_previous_'):\ncontinue\nif key.endswith('_set'):\ncontinue\nif not hasattr(model, key):\ncontinue\nattr = getattr(model, key)\nif attr.__class__.__name__ == 'method':\ncontinue\nif isinstance(attr, Model):\nresult[key] = f'{attr.__class__.__name__}({self._get_pk_name(attr)}={self._repr_pk(attr.pk)})'\nelif attr.__class__.__name__ == 'ManyRelatedManager':\ninstances = [\nf'{attr.model.__name__}({self._get_pk_name(x)}={self._repr_pk(x.pk)})'\nfor x in attr.get_queryset()\n]\nresult[key] = instances\nreturn (f'{model.__class__.__name__}({pk_name}={self._repr_pk(model.pk)})', result)\ndef _repr_pk(self, pk: str | int) -> int | str:\nif isinstance(pk, int):\nreturn pk\nreturn f'\"{pk}\"'\ndef _get_pk_name(self, model: Model):\nfrom django.db.models.fields import Field, SlugField\nattrs = [\nx for x in dir(model)\nif hasattr(model.__class__, x) and (isinstance(getattr(model.__class__, x), SlugField)\nor isinstance(getattr(model.__class__, x), SlugField))\nand getattr(model.__class__, x).primary_key\n]\nfor key in dir(model):\nif (hasattr(model.__class__, key) and hasattr(getattr(model.__class__, key), 'field')\nand getattr(model.__class__, key).field.primary_key):\nreturn key\nreturn 'pk'\ndef from_base64(self, hash: str | bytes) -> str:\n\"\"\"\n        Transform a base64 hash to string.\n        \"\"\"\nif isinstance(hash, str):\nhash = hash.encode()\nreturn base64.b64decode(hash).decode(ENCODE)\ndef to_base64(self, string: str | bytes) -> str:\n\"\"\"\n        Transform a base64 hash to string.\n        \"\"\"\nif isinstance(string, str):\nstring = string.encode()\nreturn base64.b64encode(string).decode(ENCODE)\ndef to_querystring(self, params: dict) -> str:\n\"\"\"\n        Transform dict to querystring\n        \"\"\"\nreturn urllib.parse.urlencode(params)\ndef from_bytes(self, s: bytes, encode: str = ENCODE) -> str:\n\"\"\"\n        Transform bytes to a string.\n        \"\"\"\nreturn s.decode(encode)\n
"},{"location":"testing/mixins/bc-format/#breathecode.tests.mixins.breathecode_mixin.format.Format.call","title":"call(*args, **kwargs)","text":"

Wraps a call into it and return its args and kwargs.

example:

args, kwargs = self.bc.format.call(2, 3, 4, a=1, b=2, c=3)\nassert args == (2, 3, 4)\nassert kwargs == {'a': 1, 'b': 2, 'c': 3}\n
Source code in breathecode/tests/mixins/breathecode_mixin/format.py
def call(self, *args: Any, **kwargs: Any) -> str:\n\"\"\"\n    Wraps a call into it and return its args and kwargs.\n    example:\n    ```py\n    args, kwargs = self.bc.format.call(2, 3, 4, a=1, b=2, c=3)\n    assert args == (2, 3, 4)\n    assert kwargs == {'a': 1, 'b': 2, 'c': 3}\n    ```\n    \"\"\"\nreturn args, kwargs\n
"},{"location":"testing/mixins/bc-format/#breathecode.tests.mixins.breathecode_mixin.format.Format.describe_models","title":"describe_models(models)","text":"

Describe the models.

Usage:

# setup the database\nmodel = self.bc.database.create(user=1, cohort=1)\n# print the docstring to the corresponding test\nself.bc.format.describe_models(model)\n
Source code in breathecode/tests/mixins/breathecode_mixin/format.py
def describe_models(self, models: dict[str, Model]) -> str:\n\"\"\"\n    Describe the models.\n    Usage:\n    ```py\n    # setup the database\n    model = self.bc.database.create(user=1, cohort=1)\n    # print the docstring to the corresponding test\n    self.bc.format.describe_models(model)\n    ```\n    \"\"\"\ntitle_spaces = ' ' * 8\nmodel_spaces = ' ' * 10\nresult = {}\nfor key in models:\nmodel = models[key]\nif isinstance(model, list):\nfor v in model:\nname, obj = self._describe_model(v)\nresult[name] = obj\nelse:\nname, obj = self._describe_model(model)\nresult[name] = obj\nprint(title_spaces + 'Descriptions of models are being generated:')\nfor line in yaml.dump(result).split('\\n'):\nif not line.startswith(' '):\nprint()\nprint(model_spaces + line)\n# This make sure the element are being printed and prevent `describe_models` are pushed to dev branch\nassert False\n
"},{"location":"testing/mixins/bc-format/#breathecode.tests.mixins.breathecode_mixin.format.Format.from_base64","title":"from_base64(hash)","text":"

Transform a base64 hash to string.

Source code in breathecode/tests/mixins/breathecode_mixin/format.py
def from_base64(self, hash: str | bytes) -> str:\n\"\"\"\n    Transform a base64 hash to string.\n    \"\"\"\nif isinstance(hash, str):\nhash = hash.encode()\nreturn base64.b64decode(hash).decode(ENCODE)\n
"},{"location":"testing/mixins/bc-format/#breathecode.tests.mixins.breathecode_mixin.format.Format.from_bytes","title":"from_bytes(s, encode=ENCODE)","text":"

Transform bytes to a string.

Source code in breathecode/tests/mixins/breathecode_mixin/format.py
def from_bytes(self, s: bytes, encode: str = ENCODE) -> str:\n\"\"\"\n    Transform bytes to a string.\n    \"\"\"\nreturn s.decode(encode)\n
"},{"location":"testing/mixins/bc-format/#breathecode.tests.mixins.breathecode_mixin.format.Format.lookup","title":"lookup(lang, overwrite=dict(), **kwargs)","text":"

Generate from lookups the values in test side to be used in querystring.

example:

query = self.bc.format.lookup(\n'en',\nstrings={\n'exact': [\n'remote_meeting_url',\n],\n},\nbools={\n'is_null': ['ended_at'],\n},\ndatetimes={\n'gte': ['starting_at'],\n'lte': ['ending_at'],\n},\nslugs=[\n'cohort_time_slot__cohort',\n'cohort_time_slot__cohort__academy',\n'cohort_time_slot__cohort__syllabus_version__syllabus',\n],\noverwrite={\n'cohort': 'cohort_time_slot__cohort',\n'academy': 'cohort_time_slot__cohort__academy',\n'syllabus': 'cohort_time_slot__cohort__syllabus_version__syllabus',\n'start': 'starting_at',\n'end': 'ending_at',\n'upcoming': 'ended_at',\n},\n)\nurl = reverse_lazy('events:me_event_liveclass') + '?' + self.bc.format.querystring(query)\n# this test avoid to pass a invalid param to ORM\nresponse = self.client.get(url)\n
Source code in breathecode/tests/mixins/breathecode_mixin/format.py
def lookup(self, lang: str, overwrite: dict = dict(), **kwargs: dict | tuple) -> dict[str, Any]:\n\"\"\"\n    Generate from lookups the values in test side to be used in querystring.\n    example:\n    ```py\n    query = self.bc.format.lookup(\n        'en',\n        strings={\n            'exact': [\n                'remote_meeting_url',\n            ],\n        },\n        bools={\n            'is_null': ['ended_at'],\n        },\n        datetimes={\n            'gte': ['starting_at'],\n            'lte': ['ending_at'],\n        },\n        slugs=[\n            'cohort_time_slot__cohort',\n            'cohort_time_slot__cohort__academy',\n            'cohort_time_slot__cohort__syllabus_version__syllabus',\n        ],\n        overwrite={\n            'cohort': 'cohort_time_slot__cohort',\n            'academy': 'cohort_time_slot__cohort__academy',\n            'syllabus': 'cohort_time_slot__cohort__syllabus_version__syllabus',\n            'start': 'starting_at',\n            'end': 'ending_at',\n            'upcoming': 'ended_at',\n        },\n    )\n    url = reverse_lazy('events:me_event_liveclass') + '?' + self.bc.format.querystring(query)\n    # this test avoid to pass a invalid param to ORM\n    response = self.client.get(url)\n    ```\n    \"\"\"\nresult = {}\n# foreign\nids = kwargs.get('ids', tuple())\nslugs = kwargs.get('slugs', tuple())\n# fields\nints = kwargs.get('ints', dict())\nstrings = kwargs.get('strings', dict())\ndatetimes = kwargs.get('datetimes', dict())\nbools = kwargs.get('bools', dict())\n# opts\ncustom_fields = kwargs.get('custom_fields', dict())\n# serialize foreign\nids = tuple(ids)\nslugs = tuple(slugs)\noverwrite = dict([(v, k) for k, v in overwrite.items()])\n# foreign\nfor field in ids:\nif field == '':\nresult['id'] = field.integer('exact')\ncontinue\nname = overwrite.get(field, field)\nresult[name] = Field.id('')\nfor field in slugs:\nif field == '':\nresult['id'] = Field.integer('exact')\nresult['slug'] = Field.string('exact')\ncontinue\nname = overwrite.get(field, field)\nresult[name] = Field.slug('')\n# fields\nfor mode in ints:\nfor field in ints[mode]:\nname = overwrite.get(field, field)\nresult[name] = Field.int(mode)\nfor mode in strings:\nfor field in strings[mode]:\nname = overwrite.get(field, field)\nresult[name] = Field.string(mode)\nfor mode in datetimes:\nfor field in datetimes[mode]:\nname = overwrite.get(field, field)\nresult[name] = Field.datetime(mode)\nfor mode in bools:\nfor field in bools[mode]:\nname = overwrite.get(field, field)\nresult[name] = Field.bool(mode)\n# custom fields\nfor field in custom_fields:\nname = overwrite.get(field, field)\nresult[name] = custom_fields[field]()\nreturn result\n
"},{"location":"testing/mixins/bc-format/#breathecode.tests.mixins.breathecode_mixin.format.Format.queryset","title":"queryset(query)","text":"

Build a QuerySet from a given dict.

Source code in breathecode/tests/mixins/breathecode_mixin/format.py
def queryset(self, query: dict) -> str:\n\"\"\"\n    Build a QuerySet from a given dict.\n    \"\"\"\nreturn Q(**query)\n
"},{"location":"testing/mixins/bc-format/#breathecode.tests.mixins.breathecode_mixin.format.Format.querystring","title":"querystring(query)","text":"

Build a querystring from a given dict.

Source code in breathecode/tests/mixins/breathecode_mixin/format.py
def querystring(self, query: dict) -> str:\n\"\"\"\n    Build a querystring from a given dict.\n    \"\"\"\nreturn urllib.parse.urlencode(query)\n
"},{"location":"testing/mixins/bc-format/#breathecode.tests.mixins.breathecode_mixin.format.Format.table","title":"table(arg)","text":"

Convert a QuerySet in a list.

Usage:

model = self.bc.database.create(user=1, group=1)\nself.bc.format.model(model.user.groups.all())  # = [{...}]\n
Source code in breathecode/tests/mixins/breathecode_mixin/format.py
def table(self, arg: QuerySet) -> dict[str, Any] | list[dict[str, Any]]:\n\"\"\"\n    Convert a QuerySet in a list.\n    Usage:\n    ```py\n    model = self.bc.database.create(user=1, group=1)\n    self.bc.format.model(model.user.groups.all())  # = [{...}]\n    ```\n    \"\"\"\nreturn [ModelsMixin.remove_dinamics_fields(self, data.__dict__.copy()) for data in arg]\n
"},{"location":"testing/mixins/bc-format/#breathecode.tests.mixins.breathecode_mixin.format.Format.to_base64","title":"to_base64(string)","text":"

Transform a base64 hash to string.

Source code in breathecode/tests/mixins/breathecode_mixin/format.py
def to_base64(self, string: str | bytes) -> str:\n\"\"\"\n    Transform a base64 hash to string.\n    \"\"\"\nif isinstance(string, str):\nstring = string.encode()\nreturn base64.b64encode(string).decode(ENCODE)\n
"},{"location":"testing/mixins/bc-format/#breathecode.tests.mixins.breathecode_mixin.format.Format.to_decimal_string","title":"to_decimal_string(decimal)","text":"

Parse a number to the django representation of a decimal.

Usage:

self.bc.format.to_decimal(1)  # returns '1.000000000000000'\n
Source code in breathecode/tests/mixins/breathecode_mixin/format.py
def to_decimal_string(self, decimal: int | float) -> str:\n\"\"\"\n    Parse a number to the django representation of a decimal.\n    Usage:\n    ```py\n    self.bc.format.to_decimal(1)  # returns '1.000000000000000'\n    ```\n    \"\"\"\nreturn '%.15f' % round(decimal, 15)\n
"},{"location":"testing/mixins/bc-format/#breathecode.tests.mixins.breathecode_mixin.format.Format.to_dict","title":"to_dict(arg)","text":"

Parse the object to a dict or list[dict].

Usage:

# setup the database, model.user is instance of dict and model.cohort\n# is instance list of dicts\nmodel = self.bc.database.create(user=1, cohort=2)\n# Parsing one model to a dict\nself.bc.format.to_dict(model.user)  # = {...}\n# Parsing many models to a list of dict (infered from the type of\n# argument)\nself.bc.format.to_dict(model.cohort)  # = [{...}, {...}]\n
Source code in breathecode/tests/mixins/breathecode_mixin/format.py
def to_dict(self, arg: Any) -> dict[str, Any] | list[dict[str, Any]]:\n\"\"\"\n    Parse the object to a `dict` or `list[dict]`.\n    Usage:\n    ```py\n    # setup the database, model.user is instance of dict and model.cohort\n    # is instance list of dicts\n    model = self.bc.database.create(user=1, cohort=2)\n    # Parsing one model to a dict\n    self.bc.format.to_dict(model.user)  # = {...}\n    # Parsing many models to a list of dict (infered from the type of\n    # argument)\n    self.bc.format.to_dict(model.cohort)  # = [{...}, {...}]\n    ```\n    \"\"\"\nif isinstance(arg, list) or isinstance(arg, QuerySet):\nreturn [self._one_to_dict(x) for x in arg]\nreturn self._one_to_dict(arg)\n
"},{"location":"testing/mixins/bc-format/#breathecode.tests.mixins.breathecode_mixin.format.Format.to_querystring","title":"to_querystring(params)","text":"

Transform dict to querystring

Source code in breathecode/tests/mixins/breathecode_mixin/format.py
def to_querystring(self, params: dict) -> str:\n\"\"\"\n    Transform dict to querystring\n    \"\"\"\nreturn urllib.parse.urlencode(params)\n
"},{"location":"testing/mixins/bc-random/","title":"bc.random","text":""},{"location":"testing/mixins/bc-random/#breathecode.tests.mixins.breathecode_mixin.random.Random","title":"Random","text":"

Mixin with the purpose of cover all the related with the custom asserts

Source code in breathecode/tests/mixins/breathecode_mixin/random.py
class Random:\n\"\"\"Mixin with the purpose of cover all the related with the custom asserts\"\"\"\n_parent: APITestCase\n_bc: interfaces.BreathecodeInterface\ndef __init__(self, parent, bc: interfaces.BreathecodeInterface) -> None:\nself._parent = parent\nself._bc = bc\ndef image(self, width: int = 10, height: int = 10, ext='png') -> tuple[TextIOWrapper, str]:\n\"\"\"\n        Generate a random image.\n        Usage:\n        ```py\n        # generate a random image with width of 20px and height of 10px\n        file, filename = self.bc.random.image(20, 10)\n        ```\n        \"\"\"\nsize = (width, height)\nfilename = fake.slug() + f'.{ext}'\nimage = Image.new('RGB', size)\narr = np.random.randint(low=0, high=255, size=(size[1], size[0]))\nimage = Image.fromarray(arr.astype('uint8'))\nimage.save(filename, IMAGE_TYPES[ext])\nfile = open(filename, 'rb')\nself._bc.garbage_collector.register_image(file)\nreturn file, filename\ndef file(self) -> tuple[TextIOWrapper, str]:\n\"\"\"\n        Generate a random file.\n        Usage:\n        ```py\n        # generate a random file\n        file, filename = self.bc.random.file()\n        ```\n        \"\"\"\next = self.string(lower=True, size=2)\nfile = tempfile.NamedTemporaryFile(suffix=f'.{ext}', delete=False)\nfile.write(os.urandom(1024))\nself._bc.garbage_collector.register_file(file)\nreturn file, file.name\ndef string(self, lower=False, upper=False, symbol=False, number=False, size=0) -> str:\nchars = ''\nif lower:\nchars = chars + string.ascii_lowercase\nif upper:\nchars = chars + string.ascii_uppercase\nif symbol:\nchars = chars + string.punctuation\nif number:\nchars = chars + string.digits\nreturn ''.join(random.choices(chars, k=size))\n
"},{"location":"testing/mixins/bc-random/#breathecode.tests.mixins.breathecode_mixin.random.Random.file","title":"file()","text":"

Generate a random file.

Usage:

# generate a random file\nfile, filename = self.bc.random.file()\n
Source code in breathecode/tests/mixins/breathecode_mixin/random.py
def file(self) -> tuple[TextIOWrapper, str]:\n\"\"\"\n    Generate a random file.\n    Usage:\n    ```py\n    # generate a random file\n    file, filename = self.bc.random.file()\n    ```\n    \"\"\"\next = self.string(lower=True, size=2)\nfile = tempfile.NamedTemporaryFile(suffix=f'.{ext}', delete=False)\nfile.write(os.urandom(1024))\nself._bc.garbage_collector.register_file(file)\nreturn file, file.name\n
"},{"location":"testing/mixins/bc-random/#breathecode.tests.mixins.breathecode_mixin.random.Random.image","title":"image(width=10, height=10, ext='png')","text":"

Generate a random image.

Usage:

# generate a random image with width of 20px and height of 10px\nfile, filename = self.bc.random.image(20, 10)\n
Source code in breathecode/tests/mixins/breathecode_mixin/random.py
def image(self, width: int = 10, height: int = 10, ext='png') -> tuple[TextIOWrapper, str]:\n\"\"\"\n    Generate a random image.\n    Usage:\n    ```py\n    # generate a random image with width of 20px and height of 10px\n    file, filename = self.bc.random.image(20, 10)\n    ```\n    \"\"\"\nsize = (width, height)\nfilename = fake.slug() + f'.{ext}'\nimage = Image.new('RGB', size)\narr = np.random.randint(low=0, high=255, size=(size[1], size[0]))\nimage = Image.fromarray(arr.astype('uint8'))\nimage.save(filename, IMAGE_TYPES[ext])\nfile = open(filename, 'rb')\nself._bc.garbage_collector.register_image(file)\nreturn file, filename\n
"},{"location":"testing/mixins/bc-request/","title":"bc.request","text":""},{"location":"testing/mixins/bc-request/#breathecode.tests.mixins.breathecode_mixin.request.Request","title":"Request","text":"

Mixin with the purpose of cover all the related with the request

Source code in breathecode/tests/mixins/breathecode_mixin/request.py
class Request:\n\"\"\"Mixin with the purpose of cover all the related with the request\"\"\"\n_parent: APITestCase\ndef __init__(self, parent, bc) -> None:\nself._parent = parent\nself._bc = bc\ndef set_headers(self, **kargs: str) -> None:\n\"\"\"\n        Set headers.\n        ```py\n        # It set the headers with:\n        #   Academy: 1\n        #   ThingOfImportance: potato\n        self.bc.request.set_headers(academy=1, thing_of_importance='potato')\n        ```\n        \"\"\"\nheaders = {}\nitems = [\nindex for index in kargs\nif kargs[index] and (isinstance(kargs[index], str) or isinstance(kargs[index], int))\n]\nfor index in items:\nheaders[f'HTTP_{index.upper()}'] = str(kargs[index])\nself._parent.client.credentials(**headers)\ndef authenticate(self, user) -> None:\n\"\"\"\n        Forces authentication in a request inside a APITestCase.\n        Usage:\n        ```py\n        # setup the database\n        model = self.bc.database.create(user=1)\n        # that setup the request to use the credential of user passed\n        self.bc.request.authenticate(model.user)\n        ```\n        Keywords arguments:\n        - user: a instance of user model `breathecode.authenticate.models.User`\n        \"\"\"\nself._parent.client.force_authenticate(user=user)\ndef manual_authentication(self, user) -> None:\n\"\"\"\n        Generate a manual authentication using a token, this method is more slower than `authenticate`.\n        ```py\n        # setup the database\n        model = self.bc.database.create(user=1)\n        # that setup the request to use the credential with tokens of user passed\n        self.bc.request.manual_authentication(model.user)\n        ```\n        Keywords arguments:\n        - user: a instance of user model `breathecode.authenticate.models.User`.\n        \"\"\"\nfrom breathecode.authenticate.models import Token\ntoken = Token.objects.create(user=user)\nself._parent.client.credentials(HTTP_AUTHORIZATION=f'Token {token.key}')\n
"},{"location":"testing/mixins/bc-request/#breathecode.tests.mixins.breathecode_mixin.request.Request.authenticate","title":"authenticate(user)","text":"

Forces authentication in a request inside a APITestCase.

Usage:

# setup the database\nmodel = self.bc.database.create(user=1)\n# that setup the request to use the credential of user passed\nself.bc.request.authenticate(model.user)\n

Keywords arguments:

  • user: a instance of user model breathecode.authenticate.models.User
Source code in breathecode/tests/mixins/breathecode_mixin/request.py
def authenticate(self, user) -> None:\n\"\"\"\n    Forces authentication in a request inside a APITestCase.\n    Usage:\n    ```py\n    # setup the database\n    model = self.bc.database.create(user=1)\n    # that setup the request to use the credential of user passed\n    self.bc.request.authenticate(model.user)\n    ```\n    Keywords arguments:\n    - user: a instance of user model `breathecode.authenticate.models.User`\n    \"\"\"\nself._parent.client.force_authenticate(user=user)\n
"},{"location":"testing/mixins/bc-request/#breathecode.tests.mixins.breathecode_mixin.request.Request.manual_authentication","title":"manual_authentication(user)","text":"

Generate a manual authentication using a token, this method is more slower than authenticate.

# setup the database\nmodel = self.bc.database.create(user=1)\n# that setup the request to use the credential with tokens of user passed\nself.bc.request.manual_authentication(model.user)\n

Keywords arguments:

  • user: a instance of user model breathecode.authenticate.models.User.
Source code in breathecode/tests/mixins/breathecode_mixin/request.py
def manual_authentication(self, user) -> None:\n\"\"\"\n    Generate a manual authentication using a token, this method is more slower than `authenticate`.\n    ```py\n    # setup the database\n    model = self.bc.database.create(user=1)\n    # that setup the request to use the credential with tokens of user passed\n    self.bc.request.manual_authentication(model.user)\n    ```\n    Keywords arguments:\n    - user: a instance of user model `breathecode.authenticate.models.User`.\n    \"\"\"\nfrom breathecode.authenticate.models import Token\ntoken = Token.objects.create(user=user)\nself._parent.client.credentials(HTTP_AUTHORIZATION=f'Token {token.key}')\n
"},{"location":"testing/mixins/bc-request/#breathecode.tests.mixins.breathecode_mixin.request.Request.set_headers","title":"set_headers(**kargs)","text":"

Set headers.

# It set the headers with:\n#   Academy: 1\n#   ThingOfImportance: potato\nself.bc.request.set_headers(academy=1, thing_of_importance='potato')\n
Source code in breathecode/tests/mixins/breathecode_mixin/request.py
def set_headers(self, **kargs: str) -> None:\n\"\"\"\n    Set headers.\n    ```py\n    # It set the headers with:\n    #   Academy: 1\n    #   ThingOfImportance: potato\n    self.bc.request.set_headers(academy=1, thing_of_importance='potato')\n    ```\n    \"\"\"\nheaders = {}\nitems = [\nindex for index in kargs\nif kargs[index] and (isinstance(kargs[index], str) or isinstance(kargs[index], int))\n]\nfor index in items:\nheaders[f'HTTP_{index.upper()}'] = str(kargs[index])\nself._parent.client.credentials(**headers)\n
"},{"location":"testing/mixins/bc/","title":"bc","text":""},{"location":"testing/mixins/bc/#breathecode.tests.mixins.breathecode_mixin.breathecode.Breathecode","title":"Breathecode","text":"

Bases: BreathecodeInterface

Collection of mixins for testing purposes

Source code in breathecode/tests/mixins/breathecode_mixin/breathecode.py
class Breathecode(BreathecodeInterface):\n\"\"\"Collection of mixins for testing purposes\"\"\"\ncache: Cache\nrandom: Random\ndatetime: Datetime\nrequest: Request\ndatabase: Database\ncheck: Check\nformat: Format\nfake: Faker\ngarbage_collector: GarbageCollector\n_parent: APITestCase\ndef __init__(self, parent) -> None:\nself._parent = parent\nself.cache = Cache(parent, self)\nself.random = Random(parent, self)\nself.datetime = Datetime(parent, self)\nself.request = Request(parent, self)\nself.database = Database(parent, self)\nself.check = Check(parent, self)\nself.format = Format(parent, self)\nself.garbage_collector = GarbageCollector(parent, self)\nself.fake = fake\ndef help(self, *args) -> None:\n\"\"\"\n        Print a list of mixin with a tree style (command of Linux).\n        Usage:\n        ```py\n        # this print a tree with all the mixins\n        self.bc.help()\n        # this print just the docs of corresponding method\n        self.bc.help('bc.datetime.now')\n        ```\n        \"\"\"\nif args:\nfor arg in args:\nself._get_doctring(arg)\nelse:\nself._help_tree()\n# prevent left a `self.bc.help()` in the code\nassert False\ndef _get_doctring(self, path: str) -> None:\nparts_of_path = path.split('.')\ncurrent_path = ''\ncurrent = None\nfor part_of_path in parts_of_path:\nif not current:\nif not hasattr(self._parent, part_of_path):\ncurrent_path += f'.{part_of_path}'\nbreak\ncurrent = getattr(self._parent, part_of_path)\nelse:\nif not hasattr(current, part_of_path):\ncurrent_path += f'.{part_of_path}'\ncurrent = None\nbreak\ncurrent = getattr(current, part_of_path)\nif current:\nfrom unittest.mock import patch, MagicMock\nif callable(current):\nprint(f'self.{path}{print_arguments(current)}:')\nelse:\nprint(f'self.{path}:')\nprint()\nwith patch('sys.stdout.write', MagicMock()) as mock:\nhelp(current)\nfor args, _ in mock.call_args_list:\nif args[0] == '\\n':\nprint()\nlines = args[0].split('\\n')\nfor line in lines[3:-1]:\nprint(f'    {line}')\nelse:\nprint(f'self.{path}:')\nprint()\nprint(f'    self{current_path} not exists.')\nprint()\ndef _help_tree(self, level: int = 0, parent: Optional[dict] = None, last_item: bool = False) -> list[str]:\n\"\"\"Print a list of mixin with a tree style (command of Linux)\"\"\"\nresult: list[str] = []\nif not parent:\nresult.append('bc')\nparent = [x for x in dir(parent or self) if not x.startswith('_')]\nif last_item:\nstarts = '    ' + ('\u2502   ' * (level - 1))\nelse:\nstarts = '\u2502   ' * level\nfor key in parent:\nitem = getattr(self, key)\nif callable(item):\nresult.append(f'{starts}\u251c\u2500\u2500 {key}{print_arguments(item)}')\nelse:\nresult.append(f'{starts}\u251c\u2500\u2500 {key}')\nlast_item = parent.index(key) == len(parent) - 1\nresult = [*result, *Breathecode._help_tree(item, level + 1, item, last_item)]\nresult[-1] = result[-1].replace('  \u251c\u2500\u2500 ', '  \u2514\u2500\u2500 ')\nresult[-1] = result[-1].replace(r'\u251c\u2500\u2500 ([a-zA-Z0-9]+)$', r'\u2514\u2500\u2500 \\1')\nfor n in range(len(result) - 1, -1, -1):\nif result[n][0] == '\u251c':\nresult[n] = re.sub(r'^\u251c', r'\u2514', result[n])\nbreak\nif level == 0:\nprint('\\n'.join(result))\nreturn result\n
"},{"location":"testing/mixins/bc/#breathecode.tests.mixins.breathecode_mixin.breathecode.Breathecode.help","title":"help(*args)","text":"

Print a list of mixin with a tree style (command of Linux).

Usage:

# this print a tree with all the mixins\nself.bc.help()\n# this print just the docs of corresponding method\nself.bc.help('bc.datetime.now')\n
Source code in breathecode/tests/mixins/breathecode_mixin/breathecode.py
def help(self, *args) -> None:\n\"\"\"\n    Print a list of mixin with a tree style (command of Linux).\n    Usage:\n    ```py\n    # this print a tree with all the mixins\n    self.bc.help()\n    # this print just the docs of corresponding method\n    self.bc.help('bc.datetime.now')\n    ```\n    \"\"\"\nif args:\nfor arg in args:\nself._get_doctring(arg)\nelse:\nself._help_tree()\n# prevent left a `self.bc.help()` in the code\nassert False\n
"},{"location":"testing/mocks/mock-requests/","title":"Mock requests","text":"

Mocks for requests module

"},{"location":"testing/mocks/mock-requests/#breathecode.tests.mocks.requests.apply_requests_delete_mock","title":"apply_requests_delete_mock(endpoints=[])","text":"

Apply a mock to requests.delete.

Usage:

import requests\nfrom unittest.mock import patch, call\nfrom breathecode.tests.mocks import apply_requests_delete_mock\nfrom breathecode.is_doesnt_exists import get_eventbrite_description_for_event\n@patch('requests.delete', apply_requests_delete_mock([\n204,\n'https://www.eventbriteapi.com/v3/events/1/structured_content/',\nNone,\n]))\ndef test_xyz():\ndelete_eventbrite_descriptions_for_event(1)\nassert requests.delete.call_args_list == [\ncall('https://www.eventbriteapi.com/v3/events/1/structured_content/',\nheaders={'Authorization': f'Bearer 1234567890'},\ndata=None),\n]\n
Source code in breathecode/tests/mocks/requests/__init__.py
def apply_requests_delete_mock(endpoints=[]):\n\"\"\"\n    Apply a mock to `requests.delete`.\n    Usage:\n    ```py\n    import requests\n    from unittest.mock import patch, call\n    from breathecode.tests.mocks import apply_requests_delete_mock\n    from breathecode.is_doesnt_exists import get_eventbrite_description_for_event\n    @patch('requests.delete', apply_requests_delete_mock([\n        204,\n        'https://www.eventbriteapi.com/v3/events/1/structured_content/',\n        None,\n    ]))\n    def test_xyz():\n        delete_eventbrite_descriptions_for_event(1)\n        assert requests.delete.call_args_list == [\n            call('https://www.eventbriteapi.com/v3/events/1/structured_content/',\n                headers={'Authorization': f'Bearer 1234567890'},\n                data=None),\n        ]\n    ```\n    \"\"\"\nreturn apply_requests_mock('DELETE', endpoints)\n
"},{"location":"testing/mocks/mock-requests/#breathecode.tests.mocks.requests.apply_requests_get_mock","title":"apply_requests_get_mock(endpoints=[])","text":"

Apply a mock to requests.get.

Usage:

import requests\nfrom unittest.mock import patch, call\nfrom breathecode.tests.mocks import apply_requests_get_mock\nfrom breathecode.is_doesnt_exists import get_eventbrite_description_for_event\n@patch('requests.get', apply_requests_get_mock([\n200,\n'https://www.eventbriteapi.com/v3/events/1/structured_content/',\n{ 'data': { ... } },\n]))\ndef test_xyz():\nget_eventbrite_descriptions_for_event(1)\nassert requests.get.call_args_list == [\ncall('https://www.eventbriteapi.com/v3/events/1/structured_content/',\nheaders={'Authorization': f'Bearer 1234567890'},\ndata=None),\n]\n
Source code in breathecode/tests/mocks/requests/__init__.py
def apply_requests_get_mock(endpoints=[]):\n\"\"\"\n    Apply a mock to `requests.get`.\n    Usage:\n    ```py\n    import requests\n    from unittest.mock import patch, call\n    from breathecode.tests.mocks import apply_requests_get_mock\n    from breathecode.is_doesnt_exists import get_eventbrite_description_for_event\n    @patch('requests.get', apply_requests_get_mock([\n        200,\n        'https://www.eventbriteapi.com/v3/events/1/structured_content/',\n        { 'data': { ... } },\n    ]))\n    def test_xyz():\n        get_eventbrite_descriptions_for_event(1)\n        assert requests.get.call_args_list == [\n            call('https://www.eventbriteapi.com/v3/events/1/structured_content/',\n                headers={'Authorization': f'Bearer 1234567890'},\n                data=None),\n        ]\n    ```\n    \"\"\"\nreturn apply_requests_mock('GET', endpoints)\n
"},{"location":"testing/mocks/mock-requests/#breathecode.tests.mocks.requests.apply_requests_head_mock","title":"apply_requests_head_mock(endpoints=[])","text":"

Apply a mock to requests.head.

Usage:

import requests\nfrom unittest.mock import patch, call\nfrom breathecode.tests.mocks import apply_requests_head_mock\nfrom breathecode.is_doesnt_exists import get_eventbrite_description_for_event\n@patch('requests.head', apply_requests_head_mock([\n200,\n'https://www.eventbriteapi.com/v3/events/1/structured_content/',\nNone,\n]))\ndef test_xyz():\nget_meta_for_eventbrite_description_for_event(1)\nassert requests.head.call_args_list == [\ncall('https://www.eventbriteapi.com/v3/events/1/structured_content/',\nheaders={'Authorization': f'Bearer 1234567890'},\ndata=None),\n]\n
Source code in breathecode/tests/mocks/requests/__init__.py
def apply_requests_head_mock(endpoints=[]):\n\"\"\"\n    Apply a mock to `requests.head`.\n    Usage:\n    ```py\n    import requests\n    from unittest.mock import patch, call\n    from breathecode.tests.mocks import apply_requests_head_mock\n    from breathecode.is_doesnt_exists import get_eventbrite_description_for_event\n    @patch('requests.head', apply_requests_head_mock([\n        200,\n        'https://www.eventbriteapi.com/v3/events/1/structured_content/',\n        None,\n    ]))\n    def test_xyz():\n        get_meta_for_eventbrite_description_for_event(1)\n        assert requests.head.call_args_list == [\n            call('https://www.eventbriteapi.com/v3/events/1/structured_content/',\n                headers={'Authorization': f'Bearer 1234567890'},\n                data=None),\n        ]\n    ```\n    \"\"\"\nreturn apply_requests_mock('HEAD', endpoints)\n
"},{"location":"testing/mocks/mock-requests/#breathecode.tests.mocks.requests.apply_requests_mock","title":"apply_requests_mock(method='get', endpoints=[])","text":"

Apply Storage Blob Mock

Source code in breathecode/tests/mocks/requests/__init__.py
def apply_requests_mock(method='get', endpoints=[]):\n\"\"\"Apply Storage Blob Mock\"\"\"\nmethod = method.lower()\nREQUESTS_INSTANCES[method] = request_mock(endpoints)\nreturn REQUESTS_INSTANCES[method]\n
"},{"location":"testing/mocks/mock-requests/#breathecode.tests.mocks.requests.apply_requests_patch_mock","title":"apply_requests_patch_mock(endpoints=[])","text":"

Apply a mock to requests.patch.

Usage:

import requests\nfrom unittest.mock import patch, call\nfrom breathecode.tests.mocks import apply_requests_patch_mock\nfrom breathecode.is_doesnt_exists import get_eventbrite_description_for_event\n@patch('requests.patch', apply_requests_patch_mock([\n200,\n'https://www.eventbriteapi.com/v3/events/1/structured_content/',\nNone,\n]))\ndef test_xyz():\npatch_eventbrite_descriptions_for_event(1)\nassert requests.patch.call_args_list == [\ncall('https://www.eventbriteapi.com/v3/events/1/structured_content/',\nheaders={'Authorization': f'Bearer 1234567890'},\ndata=None),\n]\n
Source code in breathecode/tests/mocks/requests/__init__.py
def apply_requests_patch_mock(endpoints=[]):\n\"\"\"\n    Apply a mock to `requests.patch`.\n    Usage:\n    ```py\n    import requests\n    from unittest.mock import patch, call\n    from breathecode.tests.mocks import apply_requests_patch_mock\n    from breathecode.is_doesnt_exists import get_eventbrite_description_for_event\n    @patch('requests.patch', apply_requests_patch_mock([\n        200,\n        'https://www.eventbriteapi.com/v3/events/1/structured_content/',\n        None,\n    ]))\n    def test_xyz():\n        patch_eventbrite_descriptions_for_event(1)\n        assert requests.patch.call_args_list == [\n            call('https://www.eventbriteapi.com/v3/events/1/structured_content/',\n                headers={'Authorization': f'Bearer 1234567890'},\n                data=None),\n        ]\n    ```\n    \"\"\"\nreturn apply_requests_mock('PATCH', endpoints)\n
"},{"location":"testing/mocks/mock-requests/#breathecode.tests.mocks.requests.apply_requests_post_mock","title":"apply_requests_post_mock(endpoints=[])","text":"

Apply a mock to requests.post.

Usage:

import requests\nfrom unittest.mock import patch, call\nfrom breathecode.tests.mocks import apply_requests_post_mock\nfrom breathecode.is_doesnt_exists import get_eventbrite_description_for_event\n@patch('requests.post', apply_requests_post_mock([\n201,\n'https://www.eventbriteapi.com/v3/events/1/structured_content/',\n{ 'data': { ... } },\n]))\ndef test_xyz():\npost_eventbrite_descriptions_for_event(1)\nassert requests.post.call_args_list == [\ncall('https://www.eventbriteapi.com/v3/events/1/structured_content/',\nheaders={'Authorization': f'Bearer 1234567890'},\ndata=None),\n]\n
Source code in breathecode/tests/mocks/requests/__init__.py
def apply_requests_post_mock(endpoints=[]):\n\"\"\"\n    Apply a mock to `requests.post`.\n    Usage:\n    ```py\n    import requests\n    from unittest.mock import patch, call\n    from breathecode.tests.mocks import apply_requests_post_mock\n    from breathecode.is_doesnt_exists import get_eventbrite_description_for_event\n    @patch('requests.post', apply_requests_post_mock([\n        201,\n        'https://www.eventbriteapi.com/v3/events/1/structured_content/',\n        { 'data': { ... } },\n    ]))\n    def test_xyz():\n        post_eventbrite_descriptions_for_event(1)\n        assert requests.post.call_args_list == [\n            call('https://www.eventbriteapi.com/v3/events/1/structured_content/',\n                headers={'Authorization': f'Bearer 1234567890'},\n                data=None),\n        ]\n    ```\n    \"\"\"\nreturn apply_requests_mock('POST', endpoints)\n
"},{"location":"testing/mocks/mock-requests/#breathecode.tests.mocks.requests.apply_requests_put_mock","title":"apply_requests_put_mock(endpoints=[])","text":"

Apply a mock to requests.put.

Usage:

import requests\nfrom unittest.mock import patch, call\nfrom breathecode.tests.mocks import apply_requests_put_mock\nfrom breathecode.is_doesnt_exists import get_eventbrite_description_for_event\n@patch('requests.put', apply_requests_put_mock([\n200,\n'https://www.eventbriteapi.com/v3/events/1/structured_content/',\n{ 'data': { ... } },\n]))\ndef test_xyz():\nput_eventbrite_descriptions_for_event(1)\nassert requests.put.call_args_list == [\ncall('https://www.eventbriteapi.com/v3/events/1/structured_content/',\nheaders={'Authorization': f'Bearer 1234567890'},\ndata=None),\n]\n
Source code in breathecode/tests/mocks/requests/__init__.py
def apply_requests_put_mock(endpoints=[]):\n\"\"\"\n    Apply a mock to `requests.put`.\n    Usage:\n    ```py\n    import requests\n    from unittest.mock import patch, call\n    from breathecode.tests.mocks import apply_requests_put_mock\n    from breathecode.is_doesnt_exists import get_eventbrite_description_for_event\n    @patch('requests.put', apply_requests_put_mock([\n        200,\n        'https://www.eventbriteapi.com/v3/events/1/structured_content/',\n        { 'data': { ... } },\n    ]))\n    def test_xyz():\n        put_eventbrite_descriptions_for_event(1)\n        assert requests.put.call_args_list == [\n            call('https://www.eventbriteapi.com/v3/events/1/structured_content/',\n                headers={'Authorization': f'Bearer 1234567890'},\n                data=None),\n        ]\n    ```\n    \"\"\"\nreturn apply_requests_mock('PUT', endpoints)\n
"},{"location":"testing/mocks/mock-requests/#breathecode.tests.mocks.requests.apply_requests_request_mock","title":"apply_requests_request_mock(endpoints=[])","text":"

Apply a mock to requests.request.

Usage:

import requests\nfrom unittest.mock import patch, call\nfrom breathecode.tests.mocks import apply_requests_request_mock\nfrom breathecode.is_doesnt_exists import get_eventbrite_description_for_event\n@patch('requests.request', apply_requests_request_mock([\n200,\n'https://www.eventbriteapi.com/v3/events/1/structured_content/',\n{ 'data': { ... } },\n]))\ndef test_xyz():\nget_eventbrite_description_for_event(1)\nassert requests.request.call_args_list == [\ncall('GET',\n'https://www.eventbriteapi.com/v3/events/1/structured_content/',\nheaders={'Authorization': f'Bearer 1234567890'},\ndata=None),\n]\n
Source code in breathecode/tests/mocks/requests/__init__.py
def apply_requests_request_mock(endpoints=[]):\n\"\"\"\n    Apply a mock to `requests.request`.\n    Usage:\n    ```py\n    import requests\n    from unittest.mock import patch, call\n    from breathecode.tests.mocks import apply_requests_request_mock\n    from breathecode.is_doesnt_exists import get_eventbrite_description_for_event\n    @patch('requests.request', apply_requests_request_mock([\n        200,\n        'https://www.eventbriteapi.com/v3/events/1/structured_content/',\n        { 'data': { ... } },\n    ]))\n    def test_xyz():\n        get_eventbrite_description_for_event(1)\n        assert requests.request.call_args_list == [\n            call('GET',\n                'https://www.eventbriteapi.com/v3/events/1/structured_content/',\n                headers={'Authorization': f'Bearer 1234567890'},\n                data=None),\n        ]\n    ```\n    \"\"\"\nreturn apply_requests_mock('REQUEST', endpoints)\n
"},{"location":"testing/mocks/using-mocks/","title":"Using mocks","text":""},{"location":"testing/mocks/using-mocks/#mock-object","title":"Mock object","text":"

Mock objects are simulated objects that mimic the behavior of real objects in controlled ways, most often as part of a software testing initiative. A programmer typically creates a mock object to test the behavior of some other object, in much the same way that a car designer uses a crash test dummy to simulate the dynamic behavior of a human in vehicle impacts.

"},{"location":"testing/mocks/using-mocks/#how-to-apply-a-automatic-mock","title":"How to apply a automatic mock","text":""},{"location":"testing/mocks/using-mocks/#the-most-easier-way-to-create-a-mock","title":"The most easier way to create a mock","text":"

The decorator @patch.object is the best option to implement a mock

@patch.object(object_class_or_module, 'method_or_function_to_be_mocked', MagicMock())\n
"},{"location":"testing/mocks/using-mocks/#this-is-the-code-to-test","title":"This is the code to test","text":"
# utils.py\nfrom .actions import shoot_gun, kenny_s_birth, show\ndef kenny_killer(kenny_id: int) -> None:\n# get the current kenny\nkenny = Kenny.objects.filter(id=kenny_id).first()\n# see - South Park - Coon and friends\nif kenny:\nshoot_gun(kenny)\nkenny_number = kenny_s_birth()\nshow(kenny_number)\n
"},{"location":"testing/mocks/using-mocks/#this-is-a-example-of-use-of-mocks","title":"This is a example of use of mocks","text":"
from unittest.mock import MagicMock, call, patch\nfrom rest_framework.test import APITestCase\nfrom .models import Kenny\nfrom .utils import kenny_killer\nimport app.actions as actions\n# this is a wrapper that implement the kenny_s_birth static behavior to the test\ndef kenny_s_birth_mock(number: int):\ndef kenny_s_birth():\nreturn number\n# the side_effect is a function that manage the behavior of the mocked function\nreturn MagicMock(side_effect=kenny_s_birth)\nclass KennyTestSuite(APITestCase):\n# \ud83d\udd3d this function is automatically mocked\n@patch.object(actions, 'shoot_gun', MagicMock())\n# \ud83d\udd3d this function is manually mocked\n@patch.object(actions, 'kenny_s_birth', kenny_s_birth_mock(1000))\n# \ud83d\udd3d this function is automatically mocked\n@patch.object(actions, 'show', MagicMock())\ndef test_kill_kenny(self):\nkenny = Kenny()\nkenny.save()\nkenny_killer(kenny_id=1)\n# shoot_gun() is called with a kenny instance\nself.assertEqual(actions.shoot_gun.call_args_list, [call(kenny)])\n# kenny_s_birth() is called with zero arguments\nself.assertEqual(actions.kenny_s_birth.call_args_list, [call()])\n# show is called\nself.assertEqual(actions.show.call_args_list, [call(1)])\n
"}]} \ No newline at end of file diff --git a/security/capabilities/index.html b/security/capabilities/index.html new file mode 100644 index 000000000..6bbe547c6 --- /dev/null +++ b/security/capabilities/index.html @@ -0,0 +1,1414 @@ + + + + + + + + + + + + + + + + + + + + + + + + Capabilities - BreatheCode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + +

Capabilities

+

Authenticated users must belong to at least one academy with a specific role, each role has a series of capabilities that specify what any user with that role will be "capable" of doing.

+

Authenticated methods must be decorated with the @capable_of decorator in increase security validation. For example:

+
    from breathecode.utils import capable_of
+    @capable_of('crud_member')
+    def post(self, request, academy_id=None):
+        serializer = StaffPOSTSerializer(data=request.data)
+        if serializer.is_valid():
+            serializer.save()
+            return Response(serializer.data, status=status.HTTP_201_CREATED)
+        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+
+

Any view decorated with the @capable_of must be used passing an academy id either:

+
    +
  1. Anywhere on the endpoint url, E.g: path('academy/<int:academy_id>/member', MemberView.as_view()),
  2. +
  3. Or on the request header using the Academy header.
  4. +
+

Available capabilities:

+

This list is alive, it will grow and vary over time:

+
CAPABILITIES = [
+    {
+        'slug': 'read_my_academy',
+        'description': 'Read your academy information'
+    },
+    {
+        'slug': 'crud_my_academy',
+        'description': 'Read, or update your academy information (very high level, almost the academy admin)'
+    },
+    {
+        'slug': 'crud_member',
+        'description': 'Create, update or delete academy members (very high level, almost the academy admin)'
+    },
+    {
+        'slug': 'read_member',
+        'description': 'Read academy staff member information'
+    },
+    {
+        'slug': 'crud_student',
+        'description': 'Create, update or delete students'
+    },
+    {
+        'slug': 'read_student',
+        'description': 'Read student information'
+    },
+    {
+        'slug': 'read_invite',
+        'description': 'Read invites from users'
+    },
+    {
+        'slug': 'crud_invite',
+        'description': 'Create, update or delete invites from users'
+    },
+    {
+        'slug': 'invite_resend',
+        'description': 'Resent invites for user academies'
+    },
+    {
+        'slug': 'read_assignment',
+        'description': 'Read assignment information'
+    },
+    {
+        'slug':
+        'read_assignment_sensitive_details',
+        'description':
+        'The mentor in residence is allowed to see aditional info about the task, like the "delivery url"'
+    },
+    {
+        'slug': 'read_shortlink',
+        'description': 'Access the list of marketing shortlinks'
+    },
+    {
+        'slug': 'crud_shortlink',
+        'description': 'Create, update and delete marketing short links'
+    },
+    {
+        'slug': 'crud_assignment',
+        'description': 'Update assignments'
+    },
+    {
+        'slug': 'task_delivery_details',
+        'description': 'Get delivery URL for a task, that url can be sent to students for delivery'
+    },
+    {
+        'slug': 'read_certificate',
+        'description': 'List and read all academy certificates'
+    },
+    {
+        'slug': 'crud_certificate',
+        'description': 'Create, update or delete student certificates'
+    },
+    {
+        'slug': 'read_layout',
+        'description': 'Read layouts to generate new certificates'
+    },
+    {
+        'slug': 'read_syllabus',
+        'description': 'List and read syllabus information'
+    },
+    {
+        'slug': 'crud_syllabus',
+        'description': 'Create, update or delete syllabus versions'
+    },
+    {
+        'slug': 'read_organization',
+        'description': 'Read academy organization details'
+    },
+    {
+        'slug': 'crud_organization',
+        'description': 'Update, create or delete academy organization details'
+    },
+    {
+        'slug': 'read_event',
+        'description': 'List and retrieve event information'
+    },
+    {
+        'slug': 'crud_event',
+        'description': 'Create, update or delete event information'
+    },
+    {
+        'slug': 'read_all_cohort',
+        'description': 'List all the cohorts or single cohort information'
+    },
+    {
+        'slug': 'read_single_cohort',
+        'description': 'single cohort information related to a user'
+    },
+    {
+        'slug': 'crud_cohort',
+        'description': 'Create, update or delete cohort info'
+    },
+    {
+        'slug': 'read_eventcheckin',
+        'description': 'List and read all the event_checkins'
+    },
+    {
+        'slug': 'read_survey',
+        'description': 'List all the nps answers'
+    },
+    {
+        'slug': 'crud_survey',
+        'description': 'Create, update or delete surveys'
+    },
+    {
+        'slug': 'read_nps_answers',
+        'description': 'List all the nps answers'
+    },
+    {
+        'slug': 'read_lead',
+        'description': 'List all the leads'
+    },
+    {
+        'slug': 'read_won_lead',
+        'description': 'List all the won leads'
+    },
+    {
+        'slug': 'crud_lead',
+        'description': 'Create, update or delete academy leads'
+    },
+    {
+        'slug': 'read_review',
+        'description': 'Read review for a particular academy'
+    },
+    {
+        'slug': 'crud_review',
+        'description': 'Create, update or delete academy reviews'
+    },
+    {
+        'slug': 'read_media',
+        'description': 'List all the medias'
+    },
+    {
+        'slug': 'crud_media',
+        'description': 'Create, update or delete academy medias'
+    },
+    {
+        'slug': 'read_media_resolution',
+        'description': 'List all the medias resolutions'
+    },
+    {
+        'slug': 'crud_media_resolution',
+        'description': 'Create, update or delete academy media resolutions'
+    },
+    {
+        'slug': 'read_cohort_activity',
+        'description': 'Read low level activity in a cohort (attendancy, etc.)'
+    },
+    {
+        'slug': 'generate_academy_token',
+        'description': 'Create a new token only to be used by the academy'
+    },
+    {
+        'slug': 'get_academy_token',
+        'description': 'Read the academy token'
+    },
+    {
+        'slug': 'send_reset_password',
+        'description': 'Generate a temporal token and resend forgot password link'
+    },
+    {
+        'slug': 'read_activity',
+        'description': 'List all the user activities'
+    },
+    {
+        'slug': 'crud_activity',
+        'description': 'Create, update or delete a user activities'
+    },
+    {
+        'slug': 'read_assignment',
+        'description': 'List all the assignments'
+    },
+    {
+        'slug': 'crud_assignment',
+        'description': 'Create, update or delete a assignment'
+    },
+    {
+        'slug':
+        'classroom_activity',
+        'description':
+        'To report student activities during the classroom or cohorts (Specially meant for teachers)'
+    },
+    {
+        'slug': 'academy_reporting',
+        'description': 'Get detailed reports about the academy activity'
+    },
+    {
+        'slug': 'generate_temporal_token',
+        'description': 'Generate a temporal token to reset github credential or forgot password'
+    },
+    {
+        'slug': 'read_mentorship_service',
+        'description': 'Get all mentorship services from one academy'
+    },
+    {
+        'slug': 'crud_mentorship_service',
+        'description': 'Create, delete or update all mentorship services from one academy'
+    },
+    {
+        'slug': 'read_mentorship_mentor',
+        'description': 'Get all mentorship mentors from one academy'
+    },
+    {
+        'slug': 'crud_mentorship_mentor',
+        'description': 'Create, delete or update all mentorship mentors from one academy'
+    },
+    {
+        'slug': 'read_mentorship_session',
+        'description': 'Get all session from one academy'
+    },
+    {
+        'slug': 'crud_mentorship_session',
+        'description': 'Create, delete or update all session from one academy'
+    },
+    {
+        'slug': 'crud_freelancer_bill',
+        'description': 'Create, delete or update all freelancer bills from one academy'
+    },
+    {
+        'slug': 'read_freelancer_bill',
+        'description': 'Read all all freelancer bills from one academy'
+    },
+    {
+        'slug': 'crud_mentorship_bill',
+        'description': 'Create, delete or update all mentroship bills from one academy'
+    },
+    {
+        'slug': 'read_mentorship_bill',
+        'description': 'Read all mentroship bills from one academy'
+    },
+    {
+        'slug': 'read_asset',
+        'description': 'Read all academy registry assets'
+    },
+    {
+        'slug': 'crud_asset',
+        'description': 'Update, create and delete registry assets'
+    },
+    {
+        'slug': 'read_tag',
+        'description': 'Read marketing tags and their details'
+    },
+    {
+        'slug': 'crud_tag',
+        'description': 'Update, create and delete a marketing tag and its details'
+    },
+    {
+        'slug': 'get_gitpod_user',
+        'description': 'List gitpod user the academy is consuming'
+    },
+    {
+        'slug': 'update_gitpod_user',
+        'description': 'Update gitpod user expiration based on available information'
+    },
+    {
+        'slug': 'read_technology',
+        'description': 'Read asset technologies'
+    },
+    {
+        'slug': 'crud_technology',
+        'description': 'Update, create and delete asset technologies'
+    },
+    {
+        'slug': 'read_keyword',
+        'description': 'Read SEO keywords'
+    },
+    {
+        'slug': 'crud_keyword',
+        'description': 'Update, create and delete SEO keywords'
+    },
+    {
+        'slug': 'read_keywordcluster',
+        'description': 'Update, create and delete asset technologies'
+    },
+    {
+        'slug': 'crud_keywordcluster',
+        'description': 'Update, create and delete asset technologies'
+    },
+]
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/services/google_cloud/google-cloud-functions/index.html b/services/google_cloud/google-cloud-functions/index.html new file mode 100644 index 000000000..6caa745fe --- /dev/null +++ b/services/google_cloud/google-cloud-functions/index.html @@ -0,0 +1,1190 @@ + + + + + + + + + + + + + + + + + + + + + + + + Google Cloud Functions - BreatheCode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + +

Google Cloud Functions

+

Write a HTTP function

+

https://cloud.google.com/functions/docs/writing/http

+

See active functions

+

https://console.cloud.google.com/functions/list

+

Testing function

+

https://cloud.google.com/functions/docs/testing/test-http#functions-testing-http-integration-python

+

List functions

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameActivatorResourceRepository
process-zapHTTPprocess-zap
screenshotsHTTPscreenshotsjefer94/screenshots
resize-imageHTTPresize-imagebreatheco-de/gcloud-resize-image
thumbnail-generatorBucketmedia-breathecodebreatheco-de/gcloud-thumbnail-generator
thumbnail-generator-devBucketmedia-breathecode-devbreatheco-de/gcloud-thumbnail-generator
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/services/google_cloud/storage/index.html b/services/google_cloud/storage/index.html new file mode 100644 index 000000000..1b80cd12d --- /dev/null +++ b/services/google_cloud/storage/index.html @@ -0,0 +1,1346 @@ + + + + + + + + + + + + + + + + + + + + + + + + Storage - BreatheCode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + +

Storage

+ + +
+ + + +
+ + + +
+ + + + + + + + +
+ + + +

+ Storage + + +

+ + +
+ + +

Google Cloud Storage

+ + +
+ Source code in breathecode/services/google_cloud/storage.py +
11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
class Storage:
+    """Google Cloud Storage"""
+    client: storage.Client
+
+    def __init__(self) -> None:
+        # from google.cloud.storage import Client
+        credentials.resolve_credentials()
+        self.client = storage.Client()
+
+    def file(self, bucket_name: str, file_name: str) -> File:
+        """Get File object
+
+        Args:
+            bucket_name (str): Name of bucket in Google Cloud Storage
+            file_name (str): Name of blob in Google Cloud Bucket
+
+        Returns:
+            File: File object
+        """
+        bucket = self.client.bucket(bucket_name)
+        return File(bucket, file_name)
+
+
+ + + +
+ + + + + + + + + +
+ + + +

+file(bucket_name, file_name) + +

+ + +
+ +

Get File object

+ +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
bucket_name + str +

Name of bucket in Google Cloud Storage

+ required +
file_name + str +

Name of blob in Google Cloud Bucket

+ required +
+ +

Returns:

+ + + + + + + + + + + + + +
Name TypeDescription
File + File +

File object

+ +
+ Source code in breathecode/services/google_cloud/storage.py +
20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
def file(self, bucket_name: str, file_name: str) -> File:
+    """Get File object
+
+    Args:
+        bucket_name (str): Name of bucket in Google Cloud Storage
+        file_name (str): Name of blob in Google Cloud Bucket
+
+    Returns:
+        File: File object
+    """
+    bucket = self.client.bucket(bucket_name)
+    return File(bucket, file_name)
+
+
+
+ +
+ + + +
+ +
+ +
+ + + + +
+ +
+ +
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/services/slack integration/icons/index.html b/services/slack integration/icons/index.html new file mode 100644 index 000000000..845a45106 --- /dev/null +++ b/services/slack integration/icons/index.html @@ -0,0 +1,1054 @@ + + + + + + + + + + + + + + + + + + + + + + + + Icons - BreatheCode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+ +
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/signals/quickstart/index.html b/signals/quickstart/index.html new file mode 100644 index 000000000..39fc2bb2e --- /dev/null +++ b/signals/quickstart/index.html @@ -0,0 +1,1190 @@ + + + + + + + + + + + + + + + + + + + + + + + + Quickstart - BreatheCode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + +

Quickstart

+ +

Signals

+

The official documentation for django signals can be found here.

+

At BreatheCode, signals are similar concept to "events", we use signals as custom "events" that can notify important things that happen in one app to all the other app's (if they are listening).

+

For example: When a student drops from a cohort

+

There is a signal to notify when a student educational status gets updated, this is useful because other application may react to it. Here is the signal being initialized, here is being triggered/dispatched when a student gets saved and this is an example where the signal is being received on the breathecode.marketing.app to trigger some additional tasks within the system.

+

When to use a signal

+

Inside the breathecode team, we see signals for asynchronous processing of any side effects, we try to focus on them for communication between apps only.

+

Declare a new signal

+

You have many examples that you can find inside the code, each breathecode app has a file signals.py that contains all the signals dispatched by that app. If the file does not exist within one of the apps, and you need to create a signal for that app, you can create the file yourself.

+

If you wanted to create a signal for when a cohort is saved, you should start by initializing it inside breathecode/admissions/signals.py like this:

+
from django.dispatch import Signal
+
+cohort_saved = Signal()
+
+

Dispatching a signal

+

All the initialized signals are available on the same application signals.py file, if the signal you want to dispatch is not there, you should probably declare a new one.

+

After the signal is initialized, it can be dispatched anywhere withing the same app, for example inside a serializer create method like this:

+
from .signals import cohort_saved
+
+class CohortSerializer(CohortSerializerMixin):
+
+    def create(self, validated_data):
+        cohort = Cohort.objects.create(**validated_data, **self.context)
+        cohort_saved.send(instance=self, sender=CohortUser)
+        return cohort
+
+

Receiving a signal

+

All django applications can subscribe to receive a signal, even if those signals are coming from another app, but you should always add your receiving code inside the receivers.py of the app that will react to the signal.

+

The following code will receive the cohort_saved signal and print on the screen if its being created or updated.

+

Note: Its a good idea to always connect receivers to tasks, that way you can asynconosly pospone any processing that you will do after the cohort its created.

+
from breathecode.admissions.signals import student_edu_status_updated, cohort_saved
+from .models import FormEntry, ActiveCampaignAcademy
+from .tasks import add_cohort_task_to_student, add_cohort_slug_as_acp_tag
+
+@receiver(cohort_saved, sender=Cohort)
+def cohort_post_save(sender, instance, created, *args, **kwargs):
+    if created:
+        print(f"The cohort {instance.id} was just created")
+        # you can call a task from task.py here.
+    else:
+        print(f"The cohort {instance.id} was just updated")
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/sitemap.xml b/sitemap.xml new file mode 100644 index 000000000..32b88e21f --- /dev/null +++ b/sitemap.xml @@ -0,0 +1,138 @@ + + + + https://breathecode.herokuapp.com/ + 2023-07-06 + daily + + + https://breathecode.herokuapp.com/endponts/ + 2023-07-06 + daily + + + https://breathecode.herokuapp.com/apps/activities/ + 2023-07-06 + daily + + + https://breathecode.herokuapp.com/apps/admissions/ + 2023-07-06 + daily + + + https://breathecode.herokuapp.com/apps/monitoring/introduction/ + 2023-07-06 + daily + + + https://breathecode.herokuapp.com/apps/monitoring/scripts/ + 2023-07-06 + daily + + + https://breathecode.herokuapp.com/deployment/configuring-the-github-secrets/ + 2023-07-06 + daily + + + https://breathecode.herokuapp.com/deployment/environment-variables/ + 2023-07-06 + daily + + + https://breathecode.herokuapp.com/installation/environment-variables/ + 2023-07-06 + daily + + + https://breathecode.herokuapp.com/installation/fixtures/ + 2023-07-06 + daily + + + https://breathecode.herokuapp.com/security/capabilities/ + 2023-07-06 + daily + + + https://breathecode.herokuapp.com/services/google_cloud/google-cloud-functions/ + 2023-07-06 + daily + + + https://breathecode.herokuapp.com/services/google_cloud/storage/ + 2023-07-06 + daily + + + https://breathecode.herokuapp.com/services/slack%20integration/icons/ + 2023-07-06 + daily + + + https://breathecode.herokuapp.com/signals/quickstart/ + 2023-07-06 + daily + + + https://breathecode.herokuapp.com/testing/runing-tests/ + 2023-07-06 + daily + + + https://breathecode.herokuapp.com/testing/mixins/bc-cache/ + 2023-07-06 + daily + + + https://breathecode.herokuapp.com/testing/mixins/bc-check/ + 2023-07-06 + daily + + + https://breathecode.herokuapp.com/testing/mixins/bc-database/ + 2023-07-06 + daily + + + https://breathecode.herokuapp.com/testing/mixins/bc-datetime/ + 2023-07-06 + daily + + + https://breathecode.herokuapp.com/testing/mixins/bc-fake/ + 2023-07-06 + daily + + + https://breathecode.herokuapp.com/testing/mixins/bc-format/ + 2023-07-06 + daily + + + https://breathecode.herokuapp.com/testing/mixins/bc-random/ + 2023-07-06 + daily + + + https://breathecode.herokuapp.com/testing/mixins/bc-request/ + 2023-07-06 + daily + + + https://breathecode.herokuapp.com/testing/mixins/bc/ + 2023-07-06 + daily + + + https://breathecode.herokuapp.com/testing/mocks/mock-requests/ + 2023-07-06 + daily + + + https://breathecode.herokuapp.com/testing/mocks/using-mocks/ + 2023-07-06 + daily + + \ No newline at end of file diff --git a/sitemap.xml.gz b/sitemap.xml.gz new file mode 100644 index 0000000000000000000000000000000000000000..d6c9e4913813de4b470a1e9fc48b35a8eb1ba2ba GIT binary patch literal 494 zcmVMiRacY zm^l};%-BR^$q}h6e_z^3iyV3?P{7i`5(!FvBDmPQ*X!suXN*k2?~9LBU2M?X(2?)= z#rxZj@@w(hyzEkRjIvQ<&--E!nZ6ryxm+qH!4W2|LhKFOK?ODsrKziLMf0-JF&}VW zEo0|`ZRS&0k7*-<45%-~YQPwd38tyCVKh54+dXdsmN9hAw%&d!>n~;fSxp|gCo?;M z@9_}v)O0}ZwlhzfJCFGT^lxbByD50#H8bkW2CUGTgfIitZX})qas8-c@FXE>mKgF9 zLzmRe!RN%yHqx~20Pb*d;WnZdV}n0XpJwt>jmw^7NNvezBW806^k<5}&+GCGk)Un!sTWxOwf{Bec5J=XR^xJhtNCxX + + + + + + + + + + + + + + + + + + + + + + bc.cache - BreatheCode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + +

bc.cache

+ +
+ + + +
+ + + +
+ + + + + + + + +
+ + + +

+ Cache + + +

+ + +
+ + +

Mixin with the purpose of cover all the related with cache

+ + +
+ Source code in breathecode/tests/mixins/breathecode_mixin/cache.py +
11
+12
+13
+14
+15
+16
+17
+18
+19
+20
class Cache:
+    """Mixin with the purpose of cover all the related with cache"""
+
+    clear = CacheMixin.clear_cache
+    _parent: APITestCase
+    _bc: interfaces.BreathecodeInterface
+
+    def __init__(self, parent, bc: interfaces.BreathecodeInterface) -> None:
+        self._parent = parent
+        self._bc = bc
+
+
+ + + +
+ + + + + + + + + + + +
+ +
+ +
+ + + + +
+ +
+ +
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/testing/mixins/bc-check/index.html b/testing/mixins/bc-check/index.html new file mode 100644 index 000000000..4a6008be5 --- /dev/null +++ b/testing/mixins/bc-check/index.html @@ -0,0 +1,2214 @@ + + + + + + + + + + + + + + + + + + + + + + + + bc.check - BreatheCode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + +

bc.check

+ +
+ + + +
+ + + +
+ + + + + + + + +
+ + + +

+ Check + + +

+ + +
+ + +

Mixin with the purpose of cover all the related with the custom asserts

+ + +
+ Source code in breathecode/tests/mixins/breathecode_mixin/check.py +
class Check:
+    """Mixin with the purpose of cover all the related with the custom asserts"""
+
+    sha256 = Sha256Mixin.assertHash
+    token = TokenMixin.assertToken
+    _parent: APITestCase
+    _bc: interfaces.BreathecodeInterface
+
+    def __init__(self, parent, bc: interfaces.BreathecodeInterface) -> None:
+        self._parent = parent
+        self._bc = bc
+
+    def datetime_in_range(self, start: datetime, end: datetime, date: datetime) -> None:
+        """
+        Check if a range if between start and end argument.
+
+        Usage:
+
+        ```py
+        from django.utils import timezone
+
+        start = timezone.now()
+        in_range = timezone.now()
+        end = timezone.now()
+        out_of_range = timezone.now()
+
+        # pass because this datetime is between start and end
+        self.bc.check.datetime_in_range(start, end, in_range)  # 🟢
+
+        # fail because this datetime is not between start and end
+        self.bc.check.datetime_in_range(start, end, out_of_range)  # 🔴
+        ```
+        """
+
+        self._parent.assertLess(start, date)
+        self._parent.assertGreater(end, date)
+
+    def partial_equality(self, first: dict | list[dict], second: dict | list[dict]) -> None:
+        """
+        Fail if the two objects are partially unequal as determined by the '==' operator.
+
+        Usage:
+
+        ```py
+        obj1 = {'key1': 1, 'key2': 2}
+        obj2 = {'key2': 2, 'key3': 1}
+        obj3 = {'key2': 2}
+
+        # it's fail because the key3 is not in the obj1
+        self.bc.check.partial_equality(obj1, obj2)  # 🔴
+
+        # it's fail because the key1 is not in the obj2
+        self.bc.check.partial_equality(obj2, obj1)  # 🔴
+
+        # it's pass because the key2 exists in the obj1
+        self.bc.check.partial_equality(obj1, obj3)  # 🟢
+
+        # it's pass because the key2 exists in the obj2
+        self.bc.check.partial_equality(obj2, obj3)  # 🟢
+
+        # it's fail because the key1 is not in the obj3
+        self.bc.check.partial_equality(obj3, obj1)  # 🔴
+
+        # it's fail because the key3 is not in the obj3
+        self.bc.check.partial_equality(obj3, obj2)  # 🔴
+        ```
+        """
+
+        assert type(first) == type(second)
+
+        if isinstance(first, list):
+            assert len(first) == len(second)
+
+            original = []
+
+            for i in range(0, len(first)):
+                original.append(self._fill_partial_equality(first[i], second[i]))
+
+        else:
+            original = self._fill_partial_equality(first, second)
+
+        self._parent.assertEqual(original, second)
+
+    def calls(self, first: list[call], second: list[call]) -> None:
+        """
+        Fail if the two objects are partially unequal as determined by the '==' operator.
+
+        Usage:
+
+        ```py
+        self.bc.check.calls(mock.call_args_list, [call(1, 2, a=3, b=4)])
+        ```
+        """
+
+        # assert len(first) == len(second), f'not have same length than {first}\n{second}'
+        self._parent.assertEqual(len(first),
+                                 len(second),
+                                 msg=f'Does not have same length\n\n{first}\n\n!=\n\n{second}')
+
+        for i in range(0, len(first)):
+            self._parent.assertEqual(first[i].args, second[i].args, msg=f'args in index {i} does not match')
+            self._parent.assertEqual(first[i].kwargs,
+                                     second[i].kwargs,
+                                     msg=f'kwargs in index {i} does not match')
+
+    def _fill_partial_equality(self, first: dict, second: dict) -> dict:
+        original = {}
+
+        for key in second.keys():
+            original[key] = second[key]
+
+        return original
+
+    def queryset_of(self, query: Any, model: Model) -> None:
+        """
+        Check if the first argument is a queryset of a models provided as second argument.
+
+        Usage:
+
+        ```py
+        from breathecode.admissions.models import Cohort, Academy
+
+        self.bc.database.create(cohort=1)
+
+        collection = []
+        queryset = Cohort.objects.filter()
+
+        # pass because the first argument is a QuerySet and it's type Cohort
+        self.bc.check.queryset_of(queryset, Cohort)  # 🟢
+
+        # fail because the first argument is a QuerySet and it is not type Academy
+        self.bc.check.queryset_of(queryset, Academy)  # 🔴
+
+        # fail because the first argument is not a QuerySet
+        self.bc.check.queryset_of(collection, Academy)  # 🔴
+        ```
+        """
+
+        if not isinstance(query, QuerySet):
+            self._parent.fail('The first argument is not a QuerySet')
+
+        if query.model != model:
+            self._parent.fail(f'The QuerySet is type {query.model.__name__} instead of {model.__name__}')
+
+    def queryset_with_pks(self, query: Any, pks: list[int]) -> None:
+        """
+        Check if the queryset have the following primary keys.
+
+        Usage:
+
+        ```py
+        from breathecode.admissions.models import Cohort, Academy
+
+        self.bc.database.create(cohort=1)
+
+        collection = []
+        queryset = Cohort.objects.filter()
+
+        # pass because the QuerySet has the primary keys 1
+        self.bc.check.queryset_with_pks(queryset, [1])  # 🟢
+
+        # fail because the QuerySet has the primary keys 1 but the second argument is empty
+        self.bc.check.queryset_with_pks(queryset, [])  # 🔴
+        ```
+        """
+
+        if not isinstance(query, QuerySet):
+            self._parent.fail('The first argument is not a QuerySet')
+
+        self._parent.assertEqual([x.pk for x in query], pks)
+
+    def list_with_pks(self, query: Any, pks: list[int]) -> None:
+        """
+        Check if the list have the following primary keys.
+
+        Usage:
+
+        ```py
+        from breathecode.admissions.models import Cohort, Academy
+
+        model = self.bc.database.create(cohort=1)
+
+        collection = [model.cohort]
+
+        # pass because the QuerySet has the primary keys 1
+        self.bc.check.list_with_pks(collection, [1])  # 🟢
+
+        # fail because the QuerySet has the primary keys 1 but the second argument is empty
+        self.bc.check.list_with_pks(collection, [])  # 🔴
+        ```
+        """
+
+        if not isinstance(query, list):
+            self._parent.fail('The first argument is not a list')
+
+        self._parent.assertEqual([x.pk for x in query], pks)
+
+
+ + + +
+ + + + + + + + + +
+ + + +

+calls(first, second) + +

+ + +
+ +

Fail if the two objects are partially unequal as determined by the '==' operator.

+

Usage:

+
self.bc.check.calls(mock.call_args_list, [call(1, 2, a=3, b=4)])
+
+ +
+ Source code in breathecode/tests/mixins/breathecode_mixin/check.py +
def calls(self, first: list[call], second: list[call]) -> None:
+    """
+    Fail if the two objects are partially unequal as determined by the '==' operator.
+
+    Usage:
+
+    ```py
+    self.bc.check.calls(mock.call_args_list, [call(1, 2, a=3, b=4)])
+    ```
+    """
+
+    # assert len(first) == len(second), f'not have same length than {first}\n{second}'
+    self._parent.assertEqual(len(first),
+                             len(second),
+                             msg=f'Does not have same length\n\n{first}\n\n!=\n\n{second}')
+
+    for i in range(0, len(first)):
+        self._parent.assertEqual(first[i].args, second[i].args, msg=f'args in index {i} does not match')
+        self._parent.assertEqual(first[i].kwargs,
+                                 second[i].kwargs,
+                                 msg=f'kwargs in index {i} does not match')
+
+
+
+ +
+ +
+ + + +

+datetime_in_range(start, end, date) + +

+ + +
+ +

Check if a range if between start and end argument.

+

Usage:

+
from django.utils import timezone
+
+start = timezone.now()
+in_range = timezone.now()
+end = timezone.now()
+out_of_range = timezone.now()
+
+# pass because this datetime is between start and end
+self.bc.check.datetime_in_range(start, end, in_range)  # 🟢
+
+# fail because this datetime is not between start and end
+self.bc.check.datetime_in_range(start, end, out_of_range)  # 🔴
+
+ +
+ Source code in breathecode/tests/mixins/breathecode_mixin/check.py +
28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
def datetime_in_range(self, start: datetime, end: datetime, date: datetime) -> None:
+    """
+    Check if a range if between start and end argument.
+
+    Usage:
+
+    ```py
+    from django.utils import timezone
+
+    start = timezone.now()
+    in_range = timezone.now()
+    end = timezone.now()
+    out_of_range = timezone.now()
+
+    # pass because this datetime is between start and end
+    self.bc.check.datetime_in_range(start, end, in_range)  # 🟢
+
+    # fail because this datetime is not between start and end
+    self.bc.check.datetime_in_range(start, end, out_of_range)  # 🔴
+    ```
+    """
+
+    self._parent.assertLess(start, date)
+    self._parent.assertGreater(end, date)
+
+
+
+ +
+ +
+ + + +

+list_with_pks(query, pks) + +

+ + +
+ +

Check if the list have the following primary keys.

+

Usage:

+
from breathecode.admissions.models import Cohort, Academy
+
+model = self.bc.database.create(cohort=1)
+
+collection = [model.cohort]
+
+# pass because the QuerySet has the primary keys 1
+self.bc.check.list_with_pks(collection, [1])  # 🟢
+
+# fail because the QuerySet has the primary keys 1 but the second argument is empty
+self.bc.check.list_with_pks(collection, [])  # 🔴
+
+ +
+ Source code in breathecode/tests/mixins/breathecode_mixin/check.py +
def list_with_pks(self, query: Any, pks: list[int]) -> None:
+    """
+    Check if the list have the following primary keys.
+
+    Usage:
+
+    ```py
+    from breathecode.admissions.models import Cohort, Academy
+
+    model = self.bc.database.create(cohort=1)
+
+    collection = [model.cohort]
+
+    # pass because the QuerySet has the primary keys 1
+    self.bc.check.list_with_pks(collection, [1])  # 🟢
+
+    # fail because the QuerySet has the primary keys 1 but the second argument is empty
+    self.bc.check.list_with_pks(collection, [])  # 🔴
+    ```
+    """
+
+    if not isinstance(query, list):
+        self._parent.fail('The first argument is not a list')
+
+    self._parent.assertEqual([x.pk for x in query], pks)
+
+
+
+ +
+ +
+ + + +

+partial_equality(first, second) + +

+ + +
+ +

Fail if the two objects are partially unequal as determined by the '==' operator.

+

Usage:

+
obj1 = {'key1': 1, 'key2': 2}
+obj2 = {'key2': 2, 'key3': 1}
+obj3 = {'key2': 2}
+
+# it's fail because the key3 is not in the obj1
+self.bc.check.partial_equality(obj1, obj2)  # 🔴
+
+# it's fail because the key1 is not in the obj2
+self.bc.check.partial_equality(obj2, obj1)  # 🔴
+
+# it's pass because the key2 exists in the obj1
+self.bc.check.partial_equality(obj1, obj3)  # 🟢
+
+# it's pass because the key2 exists in the obj2
+self.bc.check.partial_equality(obj2, obj3)  # 🟢
+
+# it's fail because the key1 is not in the obj3
+self.bc.check.partial_equality(obj3, obj1)  # 🔴
+
+# it's fail because the key3 is not in the obj3
+self.bc.check.partial_equality(obj3, obj2)  # 🔴
+
+ +
+ Source code in breathecode/tests/mixins/breathecode_mixin/check.py +
53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
+71
+72
+73
+74
+75
+76
+77
+78
+79
+80
+81
+82
+83
+84
+85
+86
+87
+88
+89
+90
+91
+92
+93
+94
+95
+96
+97
def partial_equality(self, first: dict | list[dict], second: dict | list[dict]) -> None:
+    """
+    Fail if the two objects are partially unequal as determined by the '==' operator.
+
+    Usage:
+
+    ```py
+    obj1 = {'key1': 1, 'key2': 2}
+    obj2 = {'key2': 2, 'key3': 1}
+    obj3 = {'key2': 2}
+
+    # it's fail because the key3 is not in the obj1
+    self.bc.check.partial_equality(obj1, obj2)  # 🔴
+
+    # it's fail because the key1 is not in the obj2
+    self.bc.check.partial_equality(obj2, obj1)  # 🔴
+
+    # it's pass because the key2 exists in the obj1
+    self.bc.check.partial_equality(obj1, obj3)  # 🟢
+
+    # it's pass because the key2 exists in the obj2
+    self.bc.check.partial_equality(obj2, obj3)  # 🟢
+
+    # it's fail because the key1 is not in the obj3
+    self.bc.check.partial_equality(obj3, obj1)  # 🔴
+
+    # it's fail because the key3 is not in the obj3
+    self.bc.check.partial_equality(obj3, obj2)  # 🔴
+    ```
+    """
+
+    assert type(first) == type(second)
+
+    if isinstance(first, list):
+        assert len(first) == len(second)
+
+        original = []
+
+        for i in range(0, len(first)):
+            original.append(self._fill_partial_equality(first[i], second[i]))
+
+    else:
+        original = self._fill_partial_equality(first, second)
+
+    self._parent.assertEqual(original, second)
+
+
+
+ +
+ +
+ + + +

+queryset_of(query, model) + +

+ + +
+ +

Check if the first argument is a queryset of a models provided as second argument.

+

Usage:

+
from breathecode.admissions.models import Cohort, Academy
+
+self.bc.database.create(cohort=1)
+
+collection = []
+queryset = Cohort.objects.filter()
+
+# pass because the first argument is a QuerySet and it's type Cohort
+self.bc.check.queryset_of(queryset, Cohort)  # 🟢
+
+# fail because the first argument is a QuerySet and it is not type Academy
+self.bc.check.queryset_of(queryset, Academy)  # 🔴
+
+# fail because the first argument is not a QuerySet
+self.bc.check.queryset_of(collection, Academy)  # 🔴
+
+ +
+ Source code in breathecode/tests/mixins/breathecode_mixin/check.py +
def queryset_of(self, query: Any, model: Model) -> None:
+    """
+    Check if the first argument is a queryset of a models provided as second argument.
+
+    Usage:
+
+    ```py
+    from breathecode.admissions.models import Cohort, Academy
+
+    self.bc.database.create(cohort=1)
+
+    collection = []
+    queryset = Cohort.objects.filter()
+
+    # pass because the first argument is a QuerySet and it's type Cohort
+    self.bc.check.queryset_of(queryset, Cohort)  # 🟢
+
+    # fail because the first argument is a QuerySet and it is not type Academy
+    self.bc.check.queryset_of(queryset, Academy)  # 🔴
+
+    # fail because the first argument is not a QuerySet
+    self.bc.check.queryset_of(collection, Academy)  # 🔴
+    ```
+    """
+
+    if not isinstance(query, QuerySet):
+        self._parent.fail('The first argument is not a QuerySet')
+
+    if query.model != model:
+        self._parent.fail(f'The QuerySet is type {query.model.__name__} instead of {model.__name__}')
+
+
+
+ +
+ +
+ + + +

+queryset_with_pks(query, pks) + +

+ + +
+ +

Check if the queryset have the following primary keys.

+

Usage:

+
from breathecode.admissions.models import Cohort, Academy
+
+self.bc.database.create(cohort=1)
+
+collection = []
+queryset = Cohort.objects.filter()
+
+# pass because the QuerySet has the primary keys 1
+self.bc.check.queryset_with_pks(queryset, [1])  # 🟢
+
+# fail because the QuerySet has the primary keys 1 but the second argument is empty
+self.bc.check.queryset_with_pks(queryset, [])  # 🔴
+
+ +
+ Source code in breathecode/tests/mixins/breathecode_mixin/check.py +
def queryset_with_pks(self, query: Any, pks: list[int]) -> None:
+    """
+    Check if the queryset have the following primary keys.
+
+    Usage:
+
+    ```py
+    from breathecode.admissions.models import Cohort, Academy
+
+    self.bc.database.create(cohort=1)
+
+    collection = []
+    queryset = Cohort.objects.filter()
+
+    # pass because the QuerySet has the primary keys 1
+    self.bc.check.queryset_with_pks(queryset, [1])  # 🟢
+
+    # fail because the QuerySet has the primary keys 1 but the second argument is empty
+    self.bc.check.queryset_with_pks(queryset, [])  # 🔴
+    ```
+    """
+
+    if not isinstance(query, QuerySet):
+        self._parent.fail('The first argument is not a QuerySet')
+
+    self._parent.assertEqual([x.pk for x in query], pks)
+
+
+
+ +
+ + + +
+ +
+ +
+ + + + +
+ +
+ +
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/testing/mixins/bc-database/index.html b/testing/mixins/bc-database/index.html new file mode 100644 index 000000000..22c0c5555 --- /dev/null +++ b/testing/mixins/bc-database/index.html @@ -0,0 +1,3365 @@ + + + + + + + + + + + + + + + + + + + + + + + + bc.database - BreatheCode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + +

bc.database

+ +
+ + + +
+ + + +
+ + + + + + + + +
+ + + +

+ Database + + +

+ + +
+ + +

Mixin with the purpose of cover all the related with the database

+ + +
+ Source code in breathecode/tests/mixins/breathecode_mixin/database.py +
 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
+336
+337
+338
+339
+340
+341
+342
+343
+344
+345
+346
+347
+348
+349
+350
+351
+352
+353
+354
+355
+356
+357
+358
+359
+360
+361
+362
+363
+364
+365
+366
+367
+368
+369
+370
+371
+372
+373
+374
+375
+376
+377
+378
+379
+380
+381
+382
+383
+384
+385
+386
+387
+388
+389
+390
+391
+392
+393
+394
+395
+396
+397
+398
+399
+400
+401
+402
+403
+404
+405
+406
+407
+408
+409
+410
+411
+412
+413
+414
+415
+416
+417
+418
+419
+420
+421
+422
+423
+424
+425
+426
+427
+428
+429
+430
+431
+432
+433
+434
+435
+436
+437
+438
+439
+440
+441
+442
+443
+444
+445
+446
+447
+448
+449
+450
+451
+452
+453
+454
+455
+456
+457
+458
+459
+460
+461
+462
+463
+464
+465
+466
+467
+468
+469
+470
+471
+472
+473
+474
+475
+476
+477
+478
+479
+480
+481
+482
+483
+484
+485
+486
+487
+488
+489
+490
+491
+492
+493
+494
+495
+496
+497
+498
+499
+500
+501
+502
+503
+504
+505
+506
+507
+508
+509
+510
+511
+512
+513
+514
+515
+516
+517
+518
+519
+520
+521
+522
+523
+524
+525
+526
+527
+528
+529
+530
+531
+532
+533
+534
+535
+536
+537
+538
+539
+540
+541
+542
+543
+544
+545
+546
+547
+548
+549
+550
+551
+552
+553
+554
+555
+556
+557
+558
+559
+560
+561
+562
+563
+564
+565
+566
+567
+568
+569
+570
+571
+572
+573
+574
+575
+576
+577
+578
+579
+580
+581
+582
+583
+584
+585
+586
+587
+588
+589
+590
+591
+592
+593
+594
+595
+596
+597
+598
+599
+600
+601
class Database:
+    """Mixin with the purpose of cover all the related with the database"""
+
+    _cache: dict[str, Model] = {}
+    _parent: APITestCase
+    _bc: interfaces.BreathecodeInterface
+    how_many = 0
+
+    def __init__(self, parent, bc: interfaces.BreathecodeInterface) -> None:
+        self._parent = parent
+        self._bc = bc
+
+    def reset_queries(self):
+        reset_queries()
+
+    # @override_settings(DEBUG=True)
+    def get_queries(self, db='default'):
+        return [query['sql'] for query in connections[db].queries]
+
+    # @override_settings(DEBUG=True)
+    def print_queries(self, db='default'):
+        for query in connections[db].queries:
+            print(f'{query["time"]} {query["sql"]}\n')
+
+    @classmethod
+    def get_model(cls, path: str) -> Model:
+        """
+        Return the model matching the given app_label and model_name.
+
+        As a shortcut, app_label may be in the form <app_label>.<model_name>.
+
+        model_name is case-insensitive.
+
+        Raise LookupError if no application exists with this label, or no
+        model exists with this name in the application. Raise ValueError if
+        called with a single argument that doesn't contain exactly one dot.
+
+        Usage:
+
+        ```py
+        # class breathecode.admissions.models.Cohort
+        Cohort = self.bc.database.get_model('admissions.Cohort')
+        ```
+
+        Keywords arguments:
+        - path(`str`): path to a model, for example `admissions.CohortUser`.
+        """
+
+        if path in cls._cache:
+            return cls._cache[path]
+
+        app_label, model_name = path.split('.')
+        cls._cache[path] = apps.get_model(app_label, model_name)
+
+        return cls._cache[path]
+
+    def list_of(self, path: str, dict: bool = True) -> list[Model | dict[str, Any]]:
+        """
+        This is a wrapper for `Model.objects.filter()`, get a list of values of models as `list[dict]` if
+        `dict=True` else get a list of `Model` instances.
+
+        Usage:
+
+        ```py
+        # get all the Cohort as list of dict
+        self.bc.database.get('admissions.Cohort')
+
+        # get all the Cohort as list of instances of model
+        self.bc.database.get('admissions.Cohort', dict=False)
+        ```
+
+        Keywords arguments:
+        - path(`str`): path to a model, for example `admissions.CohortUser`.
+        - dict(`bool`): if true return dict of values of model else return model instance.
+        """
+
+        model = Database.get_model(path)
+        result = model.objects.filter()
+
+        if dict:
+            result = [ModelsMixin.remove_dinamics_fields(self, data.__dict__.copy()) for data in result]
+
+        return result
+
+    @database_sync_to_async
+    def async_list_of(self, path: str, dict: bool = True) -> list[Model | dict[str, Any]]:
+        """
+        This is a wrapper for `Model.objects.filter()`, get a list of values of models as `list[dict]` if
+        `dict=True` else get a list of `Model` instances.
+
+        Keywords arguments:
+        - path(`str`): path to a model, for example `admissions.CohortUser`.
+        - dict(`bool`): if true return dict of values of model else return model instance.
+        """
+
+        return self.list_of(path, dict)
+
+    def delete(self, path: str, pk: Optional[int | str] = None) -> tuple[int, dict[str, int]]:
+        """
+        This is a wrapper for `Model.objects.filter(pk=pk).delete()`, delete a element if `pk` is provided else
+        all the entries.
+
+        Usage:
+
+        ```py
+        # create 19110911 cohorts 🦾
+        self.bc.database.create(cohort=19110911)
+
+        # exists 19110911 cohorts 🦾
+        self.assertEqual(self.bc.database.count('admissions.Cohort'), 19110911)
+
+        # remove all the cohorts
+        self.bc.database.delete(10)
+
+        # exists 19110910 cohorts
+        self.assertEqual(self.bc.database.count('admissions.Cohort'), 19110910)
+        ```
+
+        # remove all the cohorts
+        self.bc.database.delete()
+
+        # exists 0 cohorts
+        self.assertEqual(self.bc.database.count('admissions.Cohort'), 0)
+        ```
+
+        Keywords arguments:
+        - path(`str`): path to a model, for example `admissions.CohortUser`.
+        - pk(`str | int`): primary key of model.
+        """
+
+        lookups = {'pk': pk} if pk else {}
+
+        model = Database.get_model(path)
+        return model.objects.filter(**lookups).delete()
+
+    def get(self, path: str, pk: int or str, dict: bool = True) -> Model | dict[str, Any]:
+        """
+        This is a wrapper for `Model.objects.filter(pk=pk).first()`, get the values of model as `dict` if
+        `dict=True` else get the `Model` instance.
+
+        Usage:
+
+        ```py
+        # get the Cohort with the pk 1 as dict
+        self.bc.database.get('admissions.Cohort', 1)
+
+        # get the Cohort with the pk 1 as instance of model
+        self.bc.database.get('admissions.Cohort', 1, dict=False)
+        ```
+
+        Keywords arguments:
+        - path(`str`): path to a model, for example `admissions.CohortUser`.
+        - pk(`str | int`): primary key of model.
+        - dict(`bool`): if true return dict of values of model else return model instance.
+        """
+        model = Database.get_model(path)
+        result = model.objects.filter(pk=pk).first()
+
+        if dict:
+            result = ModelsMixin.remove_dinamics_fields(self, result.__dict__.copy())
+
+        return result
+
+    @database_sync_to_async
+    def async_get(self, path: str, pk: int | str, dict: bool = True) -> Model | dict[str, Any]:
+        """
+        This is a wrapper for `Model.objects.filter(pk=pk).first()`, get the values of model as `dict` if
+        `dict=True` else get the `Model` instance.
+
+        Keywords arguments:
+        - path(`str`): path to a model, for example `admissions.CohortUser`.
+        - pk(`str | int`): primary key of model.
+        - dict(`bool`): if true return dict of values of model else return model instance.
+        """
+
+        return self.get(path, pk, dict)
+
+    def count(self, path: str) -> int:
+        """
+        This is a wrapper for `Model.objects.count()`, get how many instances of this `Model` are saved.
+
+        Usage:
+
+        ```py
+        self.bc.database.count('admissions.Cohort')
+        ```
+
+        Keywords arguments:
+        - path(`str`): path to a model, for example `admissions.CohortUser`.
+        """
+        model = Database.get_model(path)
+        return model.objects.count()
+
+    @database_sync_to_async
+    def async_count(self, path: str) -> int:
+        """
+        This is a wrapper for `Model.objects.count()`, get how many instances of this `Model` are saved.
+
+        Keywords arguments:
+        - path(`str`): path to a model, for example `admissions.CohortUser`.
+        """
+
+        return self.count(path)
+
+    @cache
+    def _get_models(self) -> list[Model]:
+        values = {}
+        for key in apps.app_configs:
+            values[key] = apps.get_app_config(key).get_models()
+        return values
+
+    def camel_case_to_snake_case(self, name):
+        name = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
+        return re.sub('([a-z0-9])([A-Z])', r'\1_\2', name).lower()
+
+    def _get_model_field_info(self, model, key):
+        attr = getattr(model, key)
+        meta = vars(attr)['field'].related_model._meta
+        model = vars(attr)['field'].related_model
+        blank = attr.field.blank
+        null = attr.field.null
+
+        result = {
+            'field': key,
+            'blank': blank,
+            'null': null,
+            'app_name': meta.app_label,
+            'model_name': meta.object_name,
+            'handler': attr,
+            'model': model,
+        }
+
+        if hasattr(attr, 'through'):
+            result['custom_through'] = '_' not in attr.through.__name__
+            result['through_fields'] = attr.rel.through_fields
+
+        return result
+
+    @cache
+    def _get_models_descriptors(self) -> list[Model]:
+        values = {}
+        apps = self._get_models()
+
+        for app_key in apps:
+            values[app_key] = {}
+            models = apps[app_key]
+            for model in models:
+                values[app_key][model.__name__] = {}
+                values[app_key][model.__name__]['meta'] = {
+                    'app_name': model._meta.app_label,
+                    'model_name': model._meta.object_name,
+                    'model': model,
+                }
+
+                values[app_key][model.__name__]['to_one'] = [
+                    self._get_model_field_info(model, x) for x in dir(model)
+                    if isinstance(getattr(model, x), ForwardManyToOneDescriptor)
+                ]
+
+                values[app_key][model.__name__]['to_many'] = [
+                    self._get_model_field_info(model, x) for x in dir(model)
+                    if isinstance(getattr(model, x), ManyToManyDescriptor)
+                ]
+
+        return values
+
+    @cache
+    def _get_models_dependencies(self) -> list[Model]:
+        values = {}
+        descriptors = self._get_models_descriptors()
+        for app_key in descriptors:
+            for descriptor_key in descriptors[app_key]:
+                descriptor = descriptors[app_key][descriptor_key]
+
+                if app_key not in values:
+                    values[app_key] = set()
+
+                primary_values = values[app_key]['primary'] if 'primary' in values[app_key] else []
+                secondary_values = values[app_key]['secondary'] if 'secondary' in values[app_key] else []
+
+                values[app_key] = {
+                    'primary': {
+                        *primary_values, *[
+                            x['app_name']
+                            for x in descriptor['to_one'] if x['app_name'] != app_key and x['null'] == False
+                        ], *[
+                            x['app_name']
+                            for x in descriptor['to_many'] if x['app_name'] != app_key and x['null'] == False
+                        ]
+                    },
+                    'secondary': {
+                        *secondary_values, *[
+                            x['app_name']
+                            for x in descriptor['to_one'] if x['app_name'] != app_key and x['null'] == True
+                        ], *[
+                            x['app_name']
+                            for x in descriptor['to_many'] if x['app_name'] != app_key and x['null'] == True
+                        ]
+                    },
+                }
+
+        return values
+
+    def _sort_models_handlers(self,
+                              dependencies_resolved=None,
+                              primary_values=None,
+                              secondary_values=None,
+                              primary_dependencies=None,
+                              secondary_dependencies=None,
+                              consume_primary=True) -> list[Model]:
+
+        dependencies_resolved = dependencies_resolved or set()
+        primary_values = primary_values or []
+        secondary_values = secondary_values or []
+
+        if not primary_dependencies and not secondary_dependencies:
+            dependencies = self._get_models_dependencies()
+
+            primary_dependencies = {}
+            for x in dependencies:
+                primary_dependencies[x] = dependencies[x]['primary']
+
+            secondary_dependencies = {}
+            for x in dependencies:
+                secondary_dependencies[x] = dependencies[x]['secondary']
+
+        for dependency in dependencies_resolved:
+            for key in primary_dependencies:
+
+                if dependency in primary_dependencies[key]:
+                    primary_dependencies[key].remove(dependency)
+
+        primary_found = [
+            x for x in [y for y in primary_dependencies if y not in dependencies_resolved]
+            if len(primary_dependencies[x]) == 0
+        ]
+
+        for x in primary_found:
+            dependencies_resolved.add(x)
+
+        secondary_found = [
+            x for x in [y for y in secondary_dependencies if y not in dependencies_resolved]
+            if len(secondary_dependencies[x]) == 0
+        ]
+
+        if consume_primary and primary_found:
+            primary_values.append(primary_found)
+
+        elif not consume_primary and secondary_found:
+            secondary_values.append(secondary_found)
+
+        for x in primary_found:
+            del primary_dependencies[x]
+
+            for dependency in primary_dependencies:
+                if x in primary_dependencies[dependency]:
+                    primary_dependencies[dependency].remove(x)
+
+        if primary_dependencies:
+            return self._sort_models_handlers(dependencies_resolved,
+                                              primary_values,
+                                              secondary_values,
+                                              primary_dependencies,
+                                              secondary_dependencies,
+                                              consume_primary=True)
+
+        if secondary_dependencies:
+            return primary_values, [x for x in secondary_dependencies if len(secondary_dependencies[x])]
+
+        return primary_values, secondary_values
+
+    @cache
+    def _get_models_handlers(self) -> list[Model]:
+        arguments = {}
+        arguments_banned = set()
+        order, deferred = self._sort_models_handlers()
+        descriptors = self._get_models_descriptors()
+
+        def manage_model(models, descriptor, *args, **kwargs):
+            model_field_name = self.camel_case_to_snake_case(descriptor['meta']['model_name'])
+            app_name = descriptor['meta']['app_name']
+            model_name = descriptor['meta']['model_name']
+
+            if model_field_name in kwargs and f'{app_name}__{model_field_name}' in kwargs:
+                raise Exception(f'Exists many apps with the same model name `{model_name}`, please use '
+                                f'`{app_name}__{model_field_name}` instead of `{model_field_name}`')
+
+            arg = False
+            if f'{app_name}__{model_field_name}' in kwargs:
+                arg = kwargs[f'{app_name}__{model_field_name}']
+
+            elif model_field_name in kwargs:
+                arg = kwargs[model_field_name]
+
+            if not model_field_name in models and is_valid(arg):
+                kargs = {}
+
+                for x in descriptor['to_one']:
+                    related_model_field_name = self.camel_case_to_snake_case(x['model_name'])
+                    if related_model_field_name in models:
+                        kargs[x['field']] = just_one(models[related_model_field_name])
+
+                without_through = [x for x in descriptor['to_many'] if x['custom_through'] == False]
+                for x in without_through:
+                    related_model_field_name = self.camel_case_to_snake_case(x['model_name'])
+
+                    if related_model_field_name in models:
+                        kargs[x['field']] = get_list(models[related_model_field_name])
+
+                models[model_field_name] = create_models(arg, f'{app_name}.{model_name}', **kargs)
+
+                with_through = [
+                    x for x in descriptor['to_many']
+                    if x['custom_through'] == True and not x['field'].endswith('_set')
+                ]
+                for x in with_through:
+                    related_model_field_name = self.camel_case_to_snake_case(x['model_name'])
+                    if related_model_field_name in models:
+
+                        for item in get_list(models[related_model_field_name]):
+                            through_current = x['through_fields'][0]
+                            through_related = x['through_fields'][1]
+                            through_args = {through_current: models[model_field_name], through_related: item}
+
+                            x['handler'].through.objects.create(**through_args)
+
+            return models
+
+        def link_deferred_model(models, descriptor, *args, **kwargs):
+            model_field_name = self.camel_case_to_snake_case(descriptor['meta']['model_name'])
+            app_name = descriptor['meta']['app_name']
+            model_name = descriptor['meta']['model_name']
+
+            if model_field_name in kwargs and f'{app_name}__{model_field_name}' in kwargs:
+                raise Exception(f'Exists many apps with the same model name `{model_name}`, please use '
+                                f'`{app_name}__{model_field_name}` instead of `{model_field_name}`')
+
+            if model_field_name in models:
+                items = models[model_field_name] if isinstance(models[model_field_name],
+                                                               list) else [models[model_field_name]]
+                for m in items:
+
+                    for x in descriptor['to_one']:
+                        related_model_field_name = self.camel_case_to_snake_case(x['model_name'])
+                        model_exists = related_model_field_name in models
+                        is_list = isinstance(models[model_field_name], list) if model_exists else False
+                        if model_exists and not is_list and not getattr(models[model_field_name], x['field']):
+                            setattr(m, x['field'], just_one(models[related_model_field_name]))
+
+                        if model_exists and is_list:
+                            for y in models[model_field_name]:
+                                if getattr(y, x['field']):
+                                    setattr(m, x['field'], just_one(models[related_model_field_name]))
+
+                    for x in descriptor['to_many']:
+                        related_model_field_name = self.camel_case_to_snake_case(x['model_name'])
+                        if related_model_field_name in models and not getattr(
+                                models[model_field_name], x['field']):
+                            setattr(m, x['field'], get_list(models[related_model_field_name]))
+
+                    setattr(m, '__mixer__', None)
+                    m.save()
+
+            return models
+
+        def wrapper(*args, **kwargs):
+            models = {}
+            for generation_round in order:
+                for app_key in generation_round:
+                    for descriptor_key in descriptors[app_key]:
+                        descriptor = descriptors[app_key][descriptor_key]
+                        attr = self.camel_case_to_snake_case(descriptor['meta']['model_name'])
+
+                        models = manage_model(models, descriptor, *args, **kwargs)
+
+                        if app_key not in arguments:
+                            arguments[app_key] = {}
+                            arguments[attr] = ...
+
+                        else:
+                            arguments_banned.add(attr)
+
+                        arguments[f'{app_key}__{attr}'] = ...
+
+            for generation_round in order:
+                for app_key in generation_round:
+                    for descriptor_key in descriptors[app_key]:
+                        descriptor = descriptors[app_key][descriptor_key]
+                        attr = self.camel_case_to_snake_case(descriptor['meta']['model_name'])
+
+                        models = link_deferred_model(models, descriptor, *args, **kwargs)
+
+                        if app_key not in arguments:
+                            arguments[app_key] = {}
+                            arguments[attr] = ...
+
+                        else:
+                            arguments_banned.add(attr)
+
+                        arguments[f'{app_key}__{attr}'] = ...
+
+            return AttrDict(**models)
+
+        return wrapper
+
+    def create_v2(self, *args, **kwargs) -> dict[str, Model | list[Model]]:
+        """
+        Unstable version of mixin that create all models, do not use this.
+        """
+        models = self._get_models_handlers()(*args, **kwargs)
+        return models
+
+    def create(self, *args, **kwargs) -> dict[str, Model | list[Model]]:
+        """
+        Create one o many instances of models and return it like a dict of models.
+
+        Usage:
+
+        ```py
+        # create three users
+        self.bc.database.create(user=3)
+
+        # create one user with a specific first name
+        user = {'first_name': 'Lacey'}
+        self.bc.database.create(user=user)
+
+        # create two users with a specific first name and last name
+        users = [
+            {'first_name': 'Lacey', 'last_name': 'Sturm'},
+            {'first_name': 'The', 'last_name': 'Warning'},
+        ]
+        self.bc.database.create(user=users)
+
+        # create two users with the same first name
+        user = {'first_name': 'Lacey'}
+        self.bc.database.create(user=(2, user))
+
+        # setting up manually the relationships
+        cohort_user = {'cohort_id': 2}
+        self.bc.database.create(cohort=2, cohort_user=cohort_user)
+        ```
+
+        It get the model name as snake case, you can pass a `bool`, `int`, `dict`, `tuple`, `list[dict]` or
+        `list[tuple]`.
+
+        Behavior for type of argument:
+
+        - `bool`: if it is true generate a instance of a model.
+        - `int`: generate a instance of a model n times, if `n` > 1 this is a list.
+        - `dict`: generate a instance of a model, this pass to mixer.blend custom values to the model.
+        - `tuple`: one element need to be a int and the other be a dict, generate a instance of a model n times,
+        if `n` > 1 this is a list, this pass to mixer.blend custom values to the model.
+        - `list[dict]`: generate a instance of a model n times, if `n` > 1 this is a list,
+        this pass to mixer.blend custom values to the model.
+        - `list[tuple]`: generate a instance of a model n times, if `n` > 1 this is a list for each element,
+        this pass to mixer.blend custom values to the model.
+
+        Keywords arguments deprecated:
+        - models: this arguments is use to implement inheritance, receive as argument the output of other
+        `self.bc.database.create()` execution.
+        - authenticate: create a user and use `APITestCase.client.force_authenticate(user=models['user'])` to
+        get credentials.
+        """
+
+        return GenerateModelsMixin.generate_models(self._parent, _new_implementation=True, *args, **kwargs)
+
+    @database_sync_to_async
+    def async_create(self, *args, **kwargs) -> dict[str, Model | list[Model]]:
+        """
+        This is a wrapper for `Model.objects.count()`, get how many instances of this `Model` are saved.
+
+        Keywords arguments:
+        - path(`str`): path to a model, for example `admissions.CohortUser`.
+        """
+
+        return self.create(*args, **kwargs)
+
+
+ + + +
+ + + + + + + + + +
+ + + +

+async_count(path) + +

+ + +
+ +

This is a wrapper for Model.objects.count(), get how many instances of this Model are saved.

+

Keywords arguments: +- path(str): path to a model, for example admissions.CohortUser.

+ +
+ Source code in breathecode/tests/mixins/breathecode_mixin/database.py +
@database_sync_to_async
+def async_count(self, path: str) -> int:
+    """
+    This is a wrapper for `Model.objects.count()`, get how many instances of this `Model` are saved.
+
+    Keywords arguments:
+    - path(`str`): path to a model, for example `admissions.CohortUser`.
+    """
+
+    return self.count(path)
+
+
+
+ +
+ +
+ + + +

+async_create(*args, **kwargs) + +

+ + +
+ +

This is a wrapper for Model.objects.count(), get how many instances of this Model are saved.

+

Keywords arguments: +- path(str): path to a model, for example admissions.CohortUser.

+ +
+ Source code in breathecode/tests/mixins/breathecode_mixin/database.py +
@database_sync_to_async
+def async_create(self, *args, **kwargs) -> dict[str, Model | list[Model]]:
+    """
+    This is a wrapper for `Model.objects.count()`, get how many instances of this `Model` are saved.
+
+    Keywords arguments:
+    - path(`str`): path to a model, for example `admissions.CohortUser`.
+    """
+
+    return self.create(*args, **kwargs)
+
+
+
+ +
+ +
+ + + +

+async_get(path, pk, dict=True) + +

+ + +
+ +

This is a wrapper for Model.objects.filter(pk=pk).first(), get the values of model as dict if +dict=True else get the Model instance.

+

Keywords arguments: +- path(str): path to a model, for example admissions.CohortUser. +- pk(str | int): primary key of model. +- dict(bool): if true return dict of values of model else return model instance.

+ +
+ Source code in breathecode/tests/mixins/breathecode_mixin/database.py +
@database_sync_to_async
+def async_get(self, path: str, pk: int | str, dict: bool = True) -> Model | dict[str, Any]:
+    """
+    This is a wrapper for `Model.objects.filter(pk=pk).first()`, get the values of model as `dict` if
+    `dict=True` else get the `Model` instance.
+
+    Keywords arguments:
+    - path(`str`): path to a model, for example `admissions.CohortUser`.
+    - pk(`str | int`): primary key of model.
+    - dict(`bool`): if true return dict of values of model else return model instance.
+    """
+
+    return self.get(path, pk, dict)
+
+
+
+ +
+ +
+ + + +

+async_list_of(path, dict=True) + +

+ + +
+ +

This is a wrapper for Model.objects.filter(), get a list of values of models as list[dict] if +dict=True else get a list of Model instances.

+

Keywords arguments: +- path(str): path to a model, for example admissions.CohortUser. +- dict(bool): if true return dict of values of model else return model instance.

+ +
+ Source code in breathecode/tests/mixins/breathecode_mixin/database.py +
@database_sync_to_async
+def async_list_of(self, path: str, dict: bool = True) -> list[Model | dict[str, Any]]:
+    """
+    This is a wrapper for `Model.objects.filter()`, get a list of values of models as `list[dict]` if
+    `dict=True` else get a list of `Model` instances.
+
+    Keywords arguments:
+    - path(`str`): path to a model, for example `admissions.CohortUser`.
+    - dict(`bool`): if true return dict of values of model else return model instance.
+    """
+
+    return self.list_of(path, dict)
+
+
+
+ +
+ +
+ + + +

+count(path) + +

+ + +
+ +

This is a wrapper for Model.objects.count(), get how many instances of this Model are saved.

+

Usage:

+
self.bc.database.count('admissions.Cohort')
+
+

Keywords arguments: +- path(str): path to a model, for example admissions.CohortUser.

+ +
+ Source code in breathecode/tests/mixins/breathecode_mixin/database.py +
def count(self, path: str) -> int:
+    """
+    This is a wrapper for `Model.objects.count()`, get how many instances of this `Model` are saved.
+
+    Usage:
+
+    ```py
+    self.bc.database.count('admissions.Cohort')
+    ```
+
+    Keywords arguments:
+    - path(`str`): path to a model, for example `admissions.CohortUser`.
+    """
+    model = Database.get_model(path)
+    return model.objects.count()
+
+
+
+ +
+ +
+ + + +

+create(*args, **kwargs) + +

+ + +
+ +

Create one o many instances of models and return it like a dict of models.

+

Usage:

+
# create three users
+self.bc.database.create(user=3)
+
+# create one user with a specific first name
+user = {'first_name': 'Lacey'}
+self.bc.database.create(user=user)
+
+# create two users with a specific first name and last name
+users = [
+    {'first_name': 'Lacey', 'last_name': 'Sturm'},
+    {'first_name': 'The', 'last_name': 'Warning'},
+]
+self.bc.database.create(user=users)
+
+# create two users with the same first name
+user = {'first_name': 'Lacey'}
+self.bc.database.create(user=(2, user))
+
+# setting up manually the relationships
+cohort_user = {'cohort_id': 2}
+self.bc.database.create(cohort=2, cohort_user=cohort_user)
+
+

It get the model name as snake case, you can pass a bool, int, dict, tuple, list[dict] or +list[tuple].

+

Behavior for type of argument:

+
    +
  • bool: if it is true generate a instance of a model.
  • +
  • int: generate a instance of a model n times, if n > 1 this is a list.
  • +
  • dict: generate a instance of a model, this pass to mixer.blend custom values to the model.
  • +
  • tuple: one element need to be a int and the other be a dict, generate a instance of a model n times, +if n > 1 this is a list, this pass to mixer.blend custom values to the model.
  • +
  • list[dict]: generate a instance of a model n times, if n > 1 this is a list, +this pass to mixer.blend custom values to the model.
  • +
  • list[tuple]: generate a instance of a model n times, if n > 1 this is a list for each element, +this pass to mixer.blend custom values to the model.
  • +
+

Keywords arguments deprecated: +- models: this arguments is use to implement inheritance, receive as argument the output of other +self.bc.database.create() execution. +- authenticate: create a user and use APITestCase.client.force_authenticate(user=models['user']) to +get credentials.

+ +
+ Source code in breathecode/tests/mixins/breathecode_mixin/database.py +
def create(self, *args, **kwargs) -> dict[str, Model | list[Model]]:
+    """
+    Create one o many instances of models and return it like a dict of models.
+
+    Usage:
+
+    ```py
+    # create three users
+    self.bc.database.create(user=3)
+
+    # create one user with a specific first name
+    user = {'first_name': 'Lacey'}
+    self.bc.database.create(user=user)
+
+    # create two users with a specific first name and last name
+    users = [
+        {'first_name': 'Lacey', 'last_name': 'Sturm'},
+        {'first_name': 'The', 'last_name': 'Warning'},
+    ]
+    self.bc.database.create(user=users)
+
+    # create two users with the same first name
+    user = {'first_name': 'Lacey'}
+    self.bc.database.create(user=(2, user))
+
+    # setting up manually the relationships
+    cohort_user = {'cohort_id': 2}
+    self.bc.database.create(cohort=2, cohort_user=cohort_user)
+    ```
+
+    It get the model name as snake case, you can pass a `bool`, `int`, `dict`, `tuple`, `list[dict]` or
+    `list[tuple]`.
+
+    Behavior for type of argument:
+
+    - `bool`: if it is true generate a instance of a model.
+    - `int`: generate a instance of a model n times, if `n` > 1 this is a list.
+    - `dict`: generate a instance of a model, this pass to mixer.blend custom values to the model.
+    - `tuple`: one element need to be a int and the other be a dict, generate a instance of a model n times,
+    if `n` > 1 this is a list, this pass to mixer.blend custom values to the model.
+    - `list[dict]`: generate a instance of a model n times, if `n` > 1 this is a list,
+    this pass to mixer.blend custom values to the model.
+    - `list[tuple]`: generate a instance of a model n times, if `n` > 1 this is a list for each element,
+    this pass to mixer.blend custom values to the model.
+
+    Keywords arguments deprecated:
+    - models: this arguments is use to implement inheritance, receive as argument the output of other
+    `self.bc.database.create()` execution.
+    - authenticate: create a user and use `APITestCase.client.force_authenticate(user=models['user'])` to
+    get credentials.
+    """
+
+    return GenerateModelsMixin.generate_models(self._parent, _new_implementation=True, *args, **kwargs)
+
+
+
+ +
+ +
+ + + +

+create_v2(*args, **kwargs) + +

+ + +
+ +

Unstable version of mixin that create all models, do not use this.

+ +
+ Source code in breathecode/tests/mixins/breathecode_mixin/database.py +
def create_v2(self, *args, **kwargs) -> dict[str, Model | list[Model]]:
+    """
+    Unstable version of mixin that create all models, do not use this.
+    """
+    models = self._get_models_handlers()(*args, **kwargs)
+    return models
+
+
+
+ +
+ +
+ + + +

+delete(path, pk=None) + +

+ + +
+ +

This is a wrapper for Model.objects.filter(pk=pk).delete(), delete a element if pk is provided else +all the entries.

+

Usage:

+
# create 19110911 cohorts 🦾
+self.bc.database.create(cohort=19110911)
+
+# exists 19110911 cohorts 🦾
+self.assertEqual(self.bc.database.count('admissions.Cohort'), 19110911)
+
+# remove all the cohorts
+self.bc.database.delete(10)
+
+# exists 19110910 cohorts
+self.assertEqual(self.bc.database.count('admissions.Cohort'), 19110910)
+
+

remove all the cohorts

+

self.bc.database.delete()

+

exists 0 cohorts

+

self.assertEqual(self.bc.database.count('admissions.Cohort'), 0) +```

+

Keywords arguments: +- path(str): path to a model, for example admissions.CohortUser. +- pk(str | int): primary key of model.

+ +
+ Source code in breathecode/tests/mixins/breathecode_mixin/database.py +
def delete(self, path: str, pk: Optional[int | str] = None) -> tuple[int, dict[str, int]]:
+    """
+    This is a wrapper for `Model.objects.filter(pk=pk).delete()`, delete a element if `pk` is provided else
+    all the entries.
+
+    Usage:
+
+    ```py
+    # create 19110911 cohorts 🦾
+    self.bc.database.create(cohort=19110911)
+
+    # exists 19110911 cohorts 🦾
+    self.assertEqual(self.bc.database.count('admissions.Cohort'), 19110911)
+
+    # remove all the cohorts
+    self.bc.database.delete(10)
+
+    # exists 19110910 cohorts
+    self.assertEqual(self.bc.database.count('admissions.Cohort'), 19110910)
+    ```
+
+    # remove all the cohorts
+    self.bc.database.delete()
+
+    # exists 0 cohorts
+    self.assertEqual(self.bc.database.count('admissions.Cohort'), 0)
+    ```
+
+    Keywords arguments:
+    - path(`str`): path to a model, for example `admissions.CohortUser`.
+    - pk(`str | int`): primary key of model.
+    """
+
+    lookups = {'pk': pk} if pk else {}
+
+    model = Database.get_model(path)
+    return model.objects.filter(**lookups).delete()
+
+
+
+ +
+ +
+ + + +

+get(path, pk, dict=True) + +

+ + +
+ +

This is a wrapper for Model.objects.filter(pk=pk).first(), get the values of model as dict if +dict=True else get the Model instance.

+

Usage:

+
# get the Cohort with the pk 1 as dict
+self.bc.database.get('admissions.Cohort', 1)
+
+# get the Cohort with the pk 1 as instance of model
+self.bc.database.get('admissions.Cohort', 1, dict=False)
+
+

Keywords arguments: +- path(str): path to a model, for example admissions.CohortUser. +- pk(str | int): primary key of model. +- dict(bool): if true return dict of values of model else return model instance.

+ +
+ Source code in breathecode/tests/mixins/breathecode_mixin/database.py +
def get(self, path: str, pk: int or str, dict: bool = True) -> Model | dict[str, Any]:
+    """
+    This is a wrapper for `Model.objects.filter(pk=pk).first()`, get the values of model as `dict` if
+    `dict=True` else get the `Model` instance.
+
+    Usage:
+
+    ```py
+    # get the Cohort with the pk 1 as dict
+    self.bc.database.get('admissions.Cohort', 1)
+
+    # get the Cohort with the pk 1 as instance of model
+    self.bc.database.get('admissions.Cohort', 1, dict=False)
+    ```
+
+    Keywords arguments:
+    - path(`str`): path to a model, for example `admissions.CohortUser`.
+    - pk(`str | int`): primary key of model.
+    - dict(`bool`): if true return dict of values of model else return model instance.
+    """
+    model = Database.get_model(path)
+    result = model.objects.filter(pk=pk).first()
+
+    if dict:
+        result = ModelsMixin.remove_dinamics_fields(self, result.__dict__.copy())
+
+    return result
+
+
+
+ +
+ +
+ + + +

+get_model(path) + + + classmethod + + +

+ + +
+ +

Return the model matching the given app_label and model_name.

+

As a shortcut, app_label may be in the form ..

+

model_name is case-insensitive.

+

Raise LookupError if no application exists with this label, or no +model exists with this name in the application. Raise ValueError if +called with a single argument that doesn't contain exactly one dot.

+

Usage:

+
# class breathecode.admissions.models.Cohort
+Cohort = self.bc.database.get_model('admissions.Cohort')
+
+

Keywords arguments: +- path(str): path to a model, for example admissions.CohortUser.

+ +
+ Source code in breathecode/tests/mixins/breathecode_mixin/database.py +
50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
+71
+72
+73
+74
+75
+76
+77
+78
+79
+80
@classmethod
+def get_model(cls, path: str) -> Model:
+    """
+    Return the model matching the given app_label and model_name.
+
+    As a shortcut, app_label may be in the form <app_label>.<model_name>.
+
+    model_name is case-insensitive.
+
+    Raise LookupError if no application exists with this label, or no
+    model exists with this name in the application. Raise ValueError if
+    called with a single argument that doesn't contain exactly one dot.
+
+    Usage:
+
+    ```py
+    # class breathecode.admissions.models.Cohort
+    Cohort = self.bc.database.get_model('admissions.Cohort')
+    ```
+
+    Keywords arguments:
+    - path(`str`): path to a model, for example `admissions.CohortUser`.
+    """
+
+    if path in cls._cache:
+        return cls._cache[path]
+
+    app_label, model_name = path.split('.')
+    cls._cache[path] = apps.get_model(app_label, model_name)
+
+    return cls._cache[path]
+
+
+
+ +
+ +
+ + + +

+list_of(path, dict=True) + +

+ + +
+ +

This is a wrapper for Model.objects.filter(), get a list of values of models as list[dict] if +dict=True else get a list of Model instances.

+

Usage:

+
# get all the Cohort as list of dict
+self.bc.database.get('admissions.Cohort')
+
+# get all the Cohort as list of instances of model
+self.bc.database.get('admissions.Cohort', dict=False)
+
+

Keywords arguments: +- path(str): path to a model, for example admissions.CohortUser. +- dict(bool): if true return dict of values of model else return model instance.

+ +
+ Source code in breathecode/tests/mixins/breathecode_mixin/database.py +
def list_of(self, path: str, dict: bool = True) -> list[Model | dict[str, Any]]:
+    """
+    This is a wrapper for `Model.objects.filter()`, get a list of values of models as `list[dict]` if
+    `dict=True` else get a list of `Model` instances.
+
+    Usage:
+
+    ```py
+    # get all the Cohort as list of dict
+    self.bc.database.get('admissions.Cohort')
+
+    # get all the Cohort as list of instances of model
+    self.bc.database.get('admissions.Cohort', dict=False)
+    ```
+
+    Keywords arguments:
+    - path(`str`): path to a model, for example `admissions.CohortUser`.
+    - dict(`bool`): if true return dict of values of model else return model instance.
+    """
+
+    model = Database.get_model(path)
+    result = model.objects.filter()
+
+    if dict:
+        result = [ModelsMixin.remove_dinamics_fields(self, data.__dict__.copy()) for data in result]
+
+    return result
+
+
+
+ +
+ + + +
+ +
+ +
+ + + + +
+ +
+ +
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/testing/mixins/bc-datetime/index.html b/testing/mixins/bc-datetime/index.html new file mode 100644 index 000000000..0a8ace642 --- /dev/null +++ b/testing/mixins/bc-datetime/index.html @@ -0,0 +1,1426 @@ + + + + + + + + + + + + + + + + + + + + + + + + bc.datetime - BreatheCode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + +

bc.datetime

+ +
+ + + +
+ + + +
+ + + + + + + + +
+ + + +

+ Datetime + + +

+ + +
+ + +

Mixin with the purpose of cover all the related with datetime

+ + +
+ Source code in breathecode/tests/mixins/breathecode_mixin/datetime.py +
13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
class Datetime:
+    """Mixin with the purpose of cover all the related with datetime"""
+
+    to_iso_string = DatetimeMixin.datetime_to_iso
+    from_iso_string = DatetimeMixin.iso_to_datetime
+    now = DatetimeMixin.datetime_now
+    _parent: APITestCase
+    _bc: interfaces.BreathecodeInterface
+
+    def __init__(self, parent, bc: interfaces.BreathecodeInterface) -> None:
+        self._parent = parent
+        self._bc = bc
+
+    def from_timedelta(self, delta=timedelta(seconds=0)) -> str:
+        """
+        Transform from timedelta to the totals seconds in str.
+
+        Usage:
+
+        ```py
+        from datetime import timedelta
+        delta = timedelta(seconds=777)
+        self.bc.datetime.from_timedelta(delta)  # equals to '777.0'
+        ```
+        """
+
+        return str(delta.total_seconds())
+
+    def to_datetime_integer(self, timezone: str, date: datetime) -> int:
+        """
+        Transform datetime to datetime integer.
+
+        Usage:
+
+        ```py
+        utc_now = timezone.now()
+
+        # date
+        date = datetime.datetime(2022, 3, 21, 2, 51, 55, 068)
+
+        # equals to 202203210751
+        self.bc.datetime.to_datetime_integer('america/new_york', date)
+        ```
+        """
+
+        return DatetimeInteger.from_datetime(timezone, date)
+
+
+ + + +
+ + + + + + + + + +
+ + + +

+from_timedelta(delta=timedelta(seconds=0)) + +

+ + +
+ +

Transform from timedelta to the totals seconds in str.

+

Usage:

+
from datetime import timedelta
+delta = timedelta(seconds=777)
+self.bc.datetime.from_timedelta(delta)  # equals to '777.0'
+
+ +
+ Source code in breathecode/tests/mixins/breathecode_mixin/datetime.py +
26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
def from_timedelta(self, delta=timedelta(seconds=0)) -> str:
+    """
+    Transform from timedelta to the totals seconds in str.
+
+    Usage:
+
+    ```py
+    from datetime import timedelta
+    delta = timedelta(seconds=777)
+    self.bc.datetime.from_timedelta(delta)  # equals to '777.0'
+    ```
+    """
+
+    return str(delta.total_seconds())
+
+
+
+ +
+ +
+ + + +

+to_datetime_integer(timezone, date) + +

+ + +
+ +

Transform datetime to datetime integer.

+

Usage:

+
utc_now = timezone.now()
+
+# date
+date = datetime.datetime(2022, 3, 21, 2, 51, 55, 068)
+
+# equals to 202203210751
+self.bc.datetime.to_datetime_integer('america/new_york', date)
+
+ +
+ Source code in breathecode/tests/mixins/breathecode_mixin/datetime.py +
41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
def to_datetime_integer(self, timezone: str, date: datetime) -> int:
+    """
+    Transform datetime to datetime integer.
+
+    Usage:
+
+    ```py
+    utc_now = timezone.now()
+
+    # date
+    date = datetime.datetime(2022, 3, 21, 2, 51, 55, 068)
+
+    # equals to 202203210751
+    self.bc.datetime.to_datetime_integer('america/new_york', date)
+    ```
+    """
+
+    return DatetimeInteger.from_datetime(timezone, date)
+
+
+
+ +
+ + + +
+ +
+ +
+ + + + +
+ +
+ +
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/testing/mixins/bc-fake/index.html b/testing/mixins/bc-fake/index.html new file mode 100644 index 000000000..b9f0151ff --- /dev/null +++ b/testing/mixins/bc-fake/index.html @@ -0,0 +1,1054 @@ + + + + + + + + + + + + + + + + + + + + + + + + bc.fake - BreatheCode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + +

bc.fake

+

Represents a instance of Faker you can learn about it in their webside

+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/testing/mixins/bc-format/index.html b/testing/mixins/bc-format/index.html new file mode 100644 index 000000000..52e1b2376 --- /dev/null +++ b/testing/mixins/bc-format/index.html @@ -0,0 +1,2914 @@ + + + + + + + + + + + + + + + + + + + + + + + + bc.format - BreatheCode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + +

bc.format

+ +
+ + + +
+ + + +
+ + + + + + + + +
+ + + +

+ Format + + +

+ + +
+ + +

Mixin with the purpose of cover all the related with format or parse something

+ + +
+ Source code in breathecode/tests/mixins/breathecode_mixin/format.py +
100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
+336
+337
+338
+339
+340
+341
+342
+343
+344
+345
+346
+347
+348
+349
+350
+351
+352
+353
+354
+355
+356
+357
+358
+359
+360
+361
+362
+363
+364
+365
+366
+367
+368
+369
+370
+371
+372
+373
+374
+375
+376
+377
+378
+379
+380
+381
+382
+383
+384
+385
+386
+387
+388
+389
+390
+391
+392
+393
+394
+395
+396
+397
+398
+399
+400
+401
+402
+403
+404
+405
+406
+407
+408
+409
+410
+411
+412
+413
+414
+415
+416
+417
+418
+419
+420
+421
+422
+423
+424
+425
+426
+427
+428
+429
+430
+431
+432
+433
+434
+435
+436
+437
+438
+439
+440
+441
+442
+443
+444
+445
+446
+447
+448
+449
+450
+451
+452
+453
+454
+455
+456
+457
+458
+459
+460
class Format:
+    """Mixin with the purpose of cover all the related with format or parse something"""
+
+    _parent: APITestCase
+    _bc: interfaces.BreathecodeInterface
+    ENCODE = ENCODE
+
+    def __init__(self, parent, bc: interfaces.BreathecodeInterface) -> None:
+        self._parent = parent
+        self._bc = bc
+
+    def call(self, *args: Any, **kwargs: Any) -> str:
+        """
+        Wraps a call into it and return its args and kwargs.
+
+        example:
+
+        ```py
+        args, kwargs = self.bc.format.call(2, 3, 4, a=1, b=2, c=3)
+
+        assert args == (2, 3, 4)
+        assert kwargs == {'a': 1, 'b': 2, 'c': 3}
+        ```
+        """
+
+        return args, kwargs
+
+    def querystring(self, query: dict) -> str:
+        """
+        Build a querystring from a given dict.
+        """
+
+        return urllib.parse.urlencode(query)
+
+    def queryset(self, query: dict) -> str:
+        """
+        Build a QuerySet from a given dict.
+        """
+
+        return Q(**query)
+
+    # remove lang from args
+    def lookup(self, lang: str, overwrite: dict = dict(), **kwargs: dict | tuple) -> dict[str, Any]:
+        """
+        Generate from lookups the values in test side to be used in querystring.
+
+        example:
+
+        ```py
+        query = self.bc.format.lookup(
+            'en',
+            strings={
+                'exact': [
+                    'remote_meeting_url',
+                ],
+            },
+            bools={
+                'is_null': ['ended_at'],
+            },
+            datetimes={
+                'gte': ['starting_at'],
+                'lte': ['ending_at'],
+            },
+            slugs=[
+                'cohort_time_slot__cohort',
+                'cohort_time_slot__cohort__academy',
+                'cohort_time_slot__cohort__syllabus_version__syllabus',
+            ],
+            overwrite={
+                'cohort': 'cohort_time_slot__cohort',
+                'academy': 'cohort_time_slot__cohort__academy',
+                'syllabus': 'cohort_time_slot__cohort__syllabus_version__syllabus',
+                'start': 'starting_at',
+                'end': 'ending_at',
+                'upcoming': 'ended_at',
+            },
+        )
+
+        url = reverse_lazy('events:me_event_liveclass') + '?' + self.bc.format.querystring(query)
+
+        # this test avoid to pass a invalid param to ORM
+        response = self.client.get(url)
+        ```
+        """
+
+        result = {}
+
+        # foreign
+        ids = kwargs.get('ids', tuple())
+        slugs = kwargs.get('slugs', tuple())
+
+        # fields
+        ints = kwargs.get('ints', dict())
+        strings = kwargs.get('strings', dict())
+        datetimes = kwargs.get('datetimes', dict())
+        bools = kwargs.get('bools', dict())
+
+        # opts
+        custom_fields = kwargs.get('custom_fields', dict())
+
+        # serialize foreign
+        ids = tuple(ids)
+        slugs = tuple(slugs)
+
+        overwrite = dict([(v, k) for k, v in overwrite.items()])
+
+        # foreign
+
+        for field in ids:
+            if field == '':
+                result['id'] = field.integer('exact')
+                continue
+
+            name = overwrite.get(field, field)
+            result[name] = Field.id('')
+
+        for field in slugs:
+            if field == '':
+                result['id'] = Field.integer('exact')
+                result['slug'] = Field.string('exact')
+                continue
+
+            name = overwrite.get(field, field)
+            result[name] = Field.slug('')
+
+        # fields
+
+        for mode in ints:
+            for field in ints[mode]:
+                name = overwrite.get(field, field)
+                result[name] = Field.int(mode)
+
+        for mode in strings:
+            for field in strings[mode]:
+                name = overwrite.get(field, field)
+                result[name] = Field.string(mode)
+
+        for mode in datetimes:
+            for field in datetimes[mode]:
+                name = overwrite.get(field, field)
+                result[name] = Field.datetime(mode)
+
+        for mode in bools:
+            for field in bools[mode]:
+                name = overwrite.get(field, field)
+                result[name] = Field.bool(mode)
+
+        # custom fields
+
+        for field in custom_fields:
+            name = overwrite.get(field, field)
+            result[name] = custom_fields[field]()
+
+        return result
+
+    def table(self, arg: QuerySet) -> dict[str, Any] | list[dict[str, Any]]:
+        """
+        Convert a QuerySet in a list.
+
+        Usage:
+
+        ```py
+        model = self.bc.database.create(user=1, group=1)
+
+        self.bc.format.model(model.user.groups.all())  # = [{...}]
+        ```
+        """
+
+        return [ModelsMixin.remove_dinamics_fields(self, data.__dict__.copy()) for data in arg]
+
+    def to_dict(self, arg: Any) -> dict[str, Any] | list[dict[str, Any]]:
+        """
+        Parse the object to a `dict` or `list[dict]`.
+
+        Usage:
+
+        ```py
+        # setup the database, model.user is instance of dict and model.cohort
+        # is instance list of dicts
+        model = self.bc.database.create(user=1, cohort=2)
+
+        # Parsing one model to a dict
+        self.bc.format.to_dict(model.user)  # = {...}
+
+        # Parsing many models to a list of dict (infered from the type of
+        # argument)
+        self.bc.format.to_dict(model.cohort)  # = [{...}, {...}]
+        ```
+        """
+
+        if isinstance(arg, list) or isinstance(arg, QuerySet):
+            return [self._one_to_dict(x) for x in arg]
+
+        return self._one_to_dict(arg)
+
+    def to_decimal_string(self, decimal: int | float) -> str:
+        """
+        Parse a number to the django representation of a decimal.
+
+        Usage:
+
+        ```py
+        self.bc.format.to_decimal(1)  # returns '1.000000000000000'
+        ```
+        """
+        return '%.15f' % round(decimal, 15)
+
+    def _one_to_dict(self, arg) -> dict[str, Any]:
+        """Parse the object to a `dict`"""
+
+        if isinstance(arg, Model):
+            return ModelsMixin.remove_dinamics_fields(None, vars(arg))
+
+        if isinstance(arg, dict):
+            return arg
+
+        raise NotImplementedError(f'{arg.__name__} is not implemented yet')
+
+    def describe_models(self, models: dict[str, Model]) -> str:
+        """
+        Describe the models.
+
+        Usage:
+
+        ```py
+        # setup the database
+        model = self.bc.database.create(user=1, cohort=1)
+
+        # print the docstring to the corresponding test
+        self.bc.format.describe_models(model)
+        ```
+        """
+
+        title_spaces = ' ' * 8
+        model_spaces = ' ' * 10
+        result = {}
+
+        for key in models:
+            model = models[key]
+
+            if isinstance(model, list):
+                for v in model:
+                    name, obj = self._describe_model(v)
+                    result[name] = obj
+
+            else:
+                name, obj = self._describe_model(model)
+                result[name] = obj
+
+        print(title_spaces + 'Descriptions of models are being generated:')
+
+        for line in yaml.dump(result).split('\n'):
+            if not line.startswith(' '):
+                print()
+
+            print(model_spaces + line)
+
+        # This make sure the element are being printed and prevent `describe_models` are pushed to dev branch
+        assert False
+
+    #TODO: this method is buggy in the line `if not hasattr(model, key)`
+    def _describe_model(self, model: Model):
+        pk_name = self._get_pk_name(model)
+        attrs = dir(model)
+        result = {}
+
+        for key in attrs:
+            if key.startswith('_'):
+                continue
+
+            if key == 'DoesNotExist':
+                continue
+
+            if key == 'MultipleObjectsReturned':
+                continue
+
+            if key.startswith('get_next_'):
+                continue
+
+            if key.startswith('get_previous_'):
+                continue
+
+            if key.endswith('_set'):
+                continue
+
+            if not hasattr(model, key):
+                continue
+
+            attr = getattr(model, key)
+
+            if attr.__class__.__name__ == 'method':
+                continue
+
+            if isinstance(attr, Model):
+                result[key] = f'{attr.__class__.__name__}({self._get_pk_name(attr)}={self._repr_pk(attr.pk)})'
+
+            elif attr.__class__.__name__ == 'ManyRelatedManager':
+                instances = [
+                    f'{attr.model.__name__}({self._get_pk_name(x)}={self._repr_pk(x.pk)})'
+                    for x in attr.get_queryset()
+                ]
+                result[key] = instances
+
+        return (f'{model.__class__.__name__}({pk_name}={self._repr_pk(model.pk)})', result)
+
+    def _repr_pk(self, pk: str | int) -> int | str:
+        if isinstance(pk, int):
+            return pk
+
+        return f'"{pk}"'
+
+    def _get_pk_name(self, model: Model):
+        from django.db.models.fields import Field, SlugField
+
+        attrs = [
+            x for x in dir(model)
+            if hasattr(model.__class__, x) and (isinstance(getattr(model.__class__, x), SlugField)
+                                                or isinstance(getattr(model.__class__, x), SlugField))
+            and getattr(model.__class__, x).primary_key
+        ]
+
+        for key in dir(model):
+            if (hasattr(model.__class__, key) and hasattr(getattr(model.__class__, key), 'field')
+                    and getattr(model.__class__, key).field.primary_key):
+                return key
+
+        return 'pk'
+
+    def from_base64(self, hash: str | bytes) -> str:
+        """
+        Transform a base64 hash to string.
+        """
+
+        if isinstance(hash, str):
+            hash = hash.encode()
+
+        return base64.b64decode(hash).decode(ENCODE)
+
+    def to_base64(self, string: str | bytes) -> str:
+        """
+        Transform a base64 hash to string.
+        """
+
+        if isinstance(string, str):
+            string = string.encode()
+
+        return base64.b64encode(string).decode(ENCODE)
+
+    def to_querystring(self, params: dict) -> str:
+        """
+        Transform dict to querystring
+        """
+
+        return urllib.parse.urlencode(params)
+
+    def from_bytes(self, s: bytes, encode: str = ENCODE) -> str:
+        """
+        Transform bytes to a string.
+        """
+
+        return s.decode(encode)
+
+
+ + + +
+ + + + + + + + + +
+ + + +

+call(*args, **kwargs) + +

+ + +
+ +

Wraps a call into it and return its args and kwargs.

+

example:

+
args, kwargs = self.bc.format.call(2, 3, 4, a=1, b=2, c=3)
+
+assert args == (2, 3, 4)
+assert kwargs == {'a': 1, 'b': 2, 'c': 3}
+
+ +
+ Source code in breathecode/tests/mixins/breathecode_mixin/format.py +
def call(self, *args: Any, **kwargs: Any) -> str:
+    """
+    Wraps a call into it and return its args and kwargs.
+
+    example:
+
+    ```py
+    args, kwargs = self.bc.format.call(2, 3, 4, a=1, b=2, c=3)
+
+    assert args == (2, 3, 4)
+    assert kwargs == {'a': 1, 'b': 2, 'c': 3}
+    ```
+    """
+
+    return args, kwargs
+
+
+
+ +
+ +
+ + + +

+describe_models(models) + +

+ + +
+ +

Describe the models.

+

Usage:

+
# setup the database
+model = self.bc.database.create(user=1, cohort=1)
+
+# print the docstring to the corresponding test
+self.bc.format.describe_models(model)
+
+ +
+ Source code in breathecode/tests/mixins/breathecode_mixin/format.py +
def describe_models(self, models: dict[str, Model]) -> str:
+    """
+    Describe the models.
+
+    Usage:
+
+    ```py
+    # setup the database
+    model = self.bc.database.create(user=1, cohort=1)
+
+    # print the docstring to the corresponding test
+    self.bc.format.describe_models(model)
+    ```
+    """
+
+    title_spaces = ' ' * 8
+    model_spaces = ' ' * 10
+    result = {}
+
+    for key in models:
+        model = models[key]
+
+        if isinstance(model, list):
+            for v in model:
+                name, obj = self._describe_model(v)
+                result[name] = obj
+
+        else:
+            name, obj = self._describe_model(model)
+            result[name] = obj
+
+    print(title_spaces + 'Descriptions of models are being generated:')
+
+    for line in yaml.dump(result).split('\n'):
+        if not line.startswith(' '):
+            print()
+
+        print(model_spaces + line)
+
+    # This make sure the element are being printed and prevent `describe_models` are pushed to dev branch
+    assert False
+
+
+
+ +
+ +
+ + + +

+from_base64(hash) + +

+ + +
+ +

Transform a base64 hash to string.

+ +
+ Source code in breathecode/tests/mixins/breathecode_mixin/format.py +
def from_base64(self, hash: str | bytes) -> str:
+    """
+    Transform a base64 hash to string.
+    """
+
+    if isinstance(hash, str):
+        hash = hash.encode()
+
+    return base64.b64decode(hash).decode(ENCODE)
+
+
+
+ +
+ +
+ + + +

+from_bytes(s, encode=ENCODE) + +

+ + +
+ +

Transform bytes to a string.

+ +
+ Source code in breathecode/tests/mixins/breathecode_mixin/format.py +
def from_bytes(self, s: bytes, encode: str = ENCODE) -> str:
+    """
+    Transform bytes to a string.
+    """
+
+    return s.decode(encode)
+
+
+
+ +
+ +
+ + + +

+lookup(lang, overwrite=dict(), **kwargs) + +

+ + +
+ +

Generate from lookups the values in test side to be used in querystring.

+

example:

+
query = self.bc.format.lookup(
+    'en',
+    strings={
+        'exact': [
+            'remote_meeting_url',
+        ],
+    },
+    bools={
+        'is_null': ['ended_at'],
+    },
+    datetimes={
+        'gte': ['starting_at'],
+        'lte': ['ending_at'],
+    },
+    slugs=[
+        'cohort_time_slot__cohort',
+        'cohort_time_slot__cohort__academy',
+        'cohort_time_slot__cohort__syllabus_version__syllabus',
+    ],
+    overwrite={
+        'cohort': 'cohort_time_slot__cohort',
+        'academy': 'cohort_time_slot__cohort__academy',
+        'syllabus': 'cohort_time_slot__cohort__syllabus_version__syllabus',
+        'start': 'starting_at',
+        'end': 'ending_at',
+        'upcoming': 'ended_at',
+    },
+)
+
+url = reverse_lazy('events:me_event_liveclass') + '?' + self.bc.format.querystring(query)
+
+# this test avoid to pass a invalid param to ORM
+response = self.client.get(url)
+
+ +
+ Source code in breathecode/tests/mixins/breathecode_mixin/format.py +
def lookup(self, lang: str, overwrite: dict = dict(), **kwargs: dict | tuple) -> dict[str, Any]:
+    """
+    Generate from lookups the values in test side to be used in querystring.
+
+    example:
+
+    ```py
+    query = self.bc.format.lookup(
+        'en',
+        strings={
+            'exact': [
+                'remote_meeting_url',
+            ],
+        },
+        bools={
+            'is_null': ['ended_at'],
+        },
+        datetimes={
+            'gte': ['starting_at'],
+            'lte': ['ending_at'],
+        },
+        slugs=[
+            'cohort_time_slot__cohort',
+            'cohort_time_slot__cohort__academy',
+            'cohort_time_slot__cohort__syllabus_version__syllabus',
+        ],
+        overwrite={
+            'cohort': 'cohort_time_slot__cohort',
+            'academy': 'cohort_time_slot__cohort__academy',
+            'syllabus': 'cohort_time_slot__cohort__syllabus_version__syllabus',
+            'start': 'starting_at',
+            'end': 'ending_at',
+            'upcoming': 'ended_at',
+        },
+    )
+
+    url = reverse_lazy('events:me_event_liveclass') + '?' + self.bc.format.querystring(query)
+
+    # this test avoid to pass a invalid param to ORM
+    response = self.client.get(url)
+    ```
+    """
+
+    result = {}
+
+    # foreign
+    ids = kwargs.get('ids', tuple())
+    slugs = kwargs.get('slugs', tuple())
+
+    # fields
+    ints = kwargs.get('ints', dict())
+    strings = kwargs.get('strings', dict())
+    datetimes = kwargs.get('datetimes', dict())
+    bools = kwargs.get('bools', dict())
+
+    # opts
+    custom_fields = kwargs.get('custom_fields', dict())
+
+    # serialize foreign
+    ids = tuple(ids)
+    slugs = tuple(slugs)
+
+    overwrite = dict([(v, k) for k, v in overwrite.items()])
+
+    # foreign
+
+    for field in ids:
+        if field == '':
+            result['id'] = field.integer('exact')
+            continue
+
+        name = overwrite.get(field, field)
+        result[name] = Field.id('')
+
+    for field in slugs:
+        if field == '':
+            result['id'] = Field.integer('exact')
+            result['slug'] = Field.string('exact')
+            continue
+
+        name = overwrite.get(field, field)
+        result[name] = Field.slug('')
+
+    # fields
+
+    for mode in ints:
+        for field in ints[mode]:
+            name = overwrite.get(field, field)
+            result[name] = Field.int(mode)
+
+    for mode in strings:
+        for field in strings[mode]:
+            name = overwrite.get(field, field)
+            result[name] = Field.string(mode)
+
+    for mode in datetimes:
+        for field in datetimes[mode]:
+            name = overwrite.get(field, field)
+            result[name] = Field.datetime(mode)
+
+    for mode in bools:
+        for field in bools[mode]:
+            name = overwrite.get(field, field)
+            result[name] = Field.bool(mode)
+
+    # custom fields
+
+    for field in custom_fields:
+        name = overwrite.get(field, field)
+        result[name] = custom_fields[field]()
+
+    return result
+
+
+
+ +
+ +
+ + + +

+queryset(query) + +

+ + +
+ +

Build a QuerySet from a given dict.

+ +
+ Source code in breathecode/tests/mixins/breathecode_mixin/format.py +
def queryset(self, query: dict) -> str:
+    """
+    Build a QuerySet from a given dict.
+    """
+
+    return Q(**query)
+
+
+
+ +
+ +
+ + + +

+querystring(query) + +

+ + +
+ +

Build a querystring from a given dict.

+ +
+ Source code in breathecode/tests/mixins/breathecode_mixin/format.py +
def querystring(self, query: dict) -> str:
+    """
+    Build a querystring from a given dict.
+    """
+
+    return urllib.parse.urlencode(query)
+
+
+
+ +
+ +
+ + + +

+table(arg) + +

+ + +
+ +

Convert a QuerySet in a list.

+

Usage:

+
model = self.bc.database.create(user=1, group=1)
+
+self.bc.format.model(model.user.groups.all())  # = [{...}]
+
+ +
+ Source code in breathecode/tests/mixins/breathecode_mixin/format.py +
def table(self, arg: QuerySet) -> dict[str, Any] | list[dict[str, Any]]:
+    """
+    Convert a QuerySet in a list.
+
+    Usage:
+
+    ```py
+    model = self.bc.database.create(user=1, group=1)
+
+    self.bc.format.model(model.user.groups.all())  # = [{...}]
+    ```
+    """
+
+    return [ModelsMixin.remove_dinamics_fields(self, data.__dict__.copy()) for data in arg]
+
+
+
+ +
+ +
+ + + +

+to_base64(string) + +

+ + +
+ +

Transform a base64 hash to string.

+ +
+ Source code in breathecode/tests/mixins/breathecode_mixin/format.py +
def to_base64(self, string: str | bytes) -> str:
+    """
+    Transform a base64 hash to string.
+    """
+
+    if isinstance(string, str):
+        string = string.encode()
+
+    return base64.b64encode(string).decode(ENCODE)
+
+
+
+ +
+ +
+ + + +

+to_decimal_string(decimal) + +

+ + +
+ +

Parse a number to the django representation of a decimal.

+

Usage:

+
self.bc.format.to_decimal(1)  # returns '1.000000000000000'
+
+ +
+ Source code in breathecode/tests/mixins/breathecode_mixin/format.py +
def to_decimal_string(self, decimal: int | float) -> str:
+    """
+    Parse a number to the django representation of a decimal.
+
+    Usage:
+
+    ```py
+    self.bc.format.to_decimal(1)  # returns '1.000000000000000'
+    ```
+    """
+    return '%.15f' % round(decimal, 15)
+
+
+
+ +
+ +
+ + + +

+to_dict(arg) + +

+ + +
+ +

Parse the object to a dict or list[dict].

+

Usage:

+
# setup the database, model.user is instance of dict and model.cohort
+# is instance list of dicts
+model = self.bc.database.create(user=1, cohort=2)
+
+# Parsing one model to a dict
+self.bc.format.to_dict(model.user)  # = {...}
+
+# Parsing many models to a list of dict (infered from the type of
+# argument)
+self.bc.format.to_dict(model.cohort)  # = [{...}, {...}]
+
+ +
+ Source code in breathecode/tests/mixins/breathecode_mixin/format.py +
def to_dict(self, arg: Any) -> dict[str, Any] | list[dict[str, Any]]:
+    """
+    Parse the object to a `dict` or `list[dict]`.
+
+    Usage:
+
+    ```py
+    # setup the database, model.user is instance of dict and model.cohort
+    # is instance list of dicts
+    model = self.bc.database.create(user=1, cohort=2)
+
+    # Parsing one model to a dict
+    self.bc.format.to_dict(model.user)  # = {...}
+
+    # Parsing many models to a list of dict (infered from the type of
+    # argument)
+    self.bc.format.to_dict(model.cohort)  # = [{...}, {...}]
+    ```
+    """
+
+    if isinstance(arg, list) or isinstance(arg, QuerySet):
+        return [self._one_to_dict(x) for x in arg]
+
+    return self._one_to_dict(arg)
+
+
+
+ +
+ +
+ + + +

+to_querystring(params) + +

+ + +
+ +

Transform dict to querystring

+ +
+ Source code in breathecode/tests/mixins/breathecode_mixin/format.py +
def to_querystring(self, params: dict) -> str:
+    """
+    Transform dict to querystring
+    """
+
+    return urllib.parse.urlencode(params)
+
+
+
+ +
+ + + +
+ +
+ +
+ + + + +
+ +
+ +
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/testing/mixins/bc-random/index.html b/testing/mixins/bc-random/index.html new file mode 100644 index 000000000..7ff1614e1 --- /dev/null +++ b/testing/mixins/bc-random/index.html @@ -0,0 +1,1500 @@ + + + + + + + + + + + + + + + + + + + + + + + + bc.random - BreatheCode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + +

bc.random

+ +
+ + + +
+ + + +
+ + + + + + + + +
+ + + +

+ Random + + +

+ + +
+ + +

Mixin with the purpose of cover all the related with the custom asserts

+ + +
+ Source code in breathecode/tests/mixins/breathecode_mixin/random.py +
23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
+71
+72
+73
+74
+75
+76
+77
+78
+79
+80
+81
+82
+83
+84
+85
+86
+87
+88
+89
+90
+91
+92
+93
+94
+95
class Random:
+    """Mixin with the purpose of cover all the related with the custom asserts"""
+
+    _parent: APITestCase
+    _bc: interfaces.BreathecodeInterface
+
+    def __init__(self, parent, bc: interfaces.BreathecodeInterface) -> None:
+        self._parent = parent
+        self._bc = bc
+
+    def image(self, width: int = 10, height: int = 10, ext='png') -> tuple[TextIOWrapper, str]:
+        """
+        Generate a random image.
+
+        Usage:
+
+        ```py
+        # generate a random image with width of 20px and height of 10px
+        file, filename = self.bc.random.image(20, 10)
+        ```
+        """
+
+        size = (width, height)
+        filename = fake.slug() + f'.{ext}'
+        image = Image.new('RGB', size)
+        arr = np.random.randint(low=0, high=255, size=(size[1], size[0]))
+
+        image = Image.fromarray(arr.astype('uint8'))
+        image.save(filename, IMAGE_TYPES[ext])
+
+        file = open(filename, 'rb')
+
+        self._bc.garbage_collector.register_image(file)
+
+        return file, filename
+
+    def file(self) -> tuple[TextIOWrapper, str]:
+        """
+        Generate a random file.
+
+        Usage:
+
+        ```py
+        # generate a random file
+        file, filename = self.bc.random.file()
+        ```
+        """
+
+        ext = self.string(lower=True, size=2)
+
+        file = tempfile.NamedTemporaryFile(suffix=f'.{ext}', delete=False)
+        file.write(os.urandom(1024))
+
+        self._bc.garbage_collector.register_file(file)
+
+        return file, file.name
+
+    def string(self, lower=False, upper=False, symbol=False, number=False, size=0) -> str:
+        chars = ''
+
+        if lower:
+            chars = chars + string.ascii_lowercase
+
+        if upper:
+            chars = chars + string.ascii_uppercase
+
+        if symbol:
+            chars = chars + string.punctuation
+
+        if number:
+            chars = chars + string.digits
+
+        return ''.join(random.choices(chars, k=size))
+
+
+ + + +
+ + + + + + + + + +
+ + + +

+file() + +

+ + +
+ +

Generate a random file.

+

Usage:

+
# generate a random file
+file, filename = self.bc.random.file()
+
+ +
+ Source code in breathecode/tests/mixins/breathecode_mixin/random.py +
59
+60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
+71
+72
+73
+74
+75
+76
+77
+78
def file(self) -> tuple[TextIOWrapper, str]:
+    """
+    Generate a random file.
+
+    Usage:
+
+    ```py
+    # generate a random file
+    file, filename = self.bc.random.file()
+    ```
+    """
+
+    ext = self.string(lower=True, size=2)
+
+    file = tempfile.NamedTemporaryFile(suffix=f'.{ext}', delete=False)
+    file.write(os.urandom(1024))
+
+    self._bc.garbage_collector.register_file(file)
+
+    return file, file.name
+
+
+
+ +
+ +
+ + + +

+image(width=10, height=10, ext='png') + +

+ + +
+ +

Generate a random image.

+

Usage:

+
# generate a random image with width of 20px and height of 10px
+file, filename = self.bc.random.image(20, 10)
+
+ +
+ Source code in breathecode/tests/mixins/breathecode_mixin/random.py +
33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
def image(self, width: int = 10, height: int = 10, ext='png') -> tuple[TextIOWrapper, str]:
+    """
+    Generate a random image.
+
+    Usage:
+
+    ```py
+    # generate a random image with width of 20px and height of 10px
+    file, filename = self.bc.random.image(20, 10)
+    ```
+    """
+
+    size = (width, height)
+    filename = fake.slug() + f'.{ext}'
+    image = Image.new('RGB', size)
+    arr = np.random.randint(low=0, high=255, size=(size[1], size[0]))
+
+    image = Image.fromarray(arr.astype('uint8'))
+    image.save(filename, IMAGE_TYPES[ext])
+
+    file = open(filename, 'rb')
+
+    self._bc.garbage_collector.register_image(file)
+
+    return file, filename
+
+
+
+ +
+ + + +
+ +
+ +
+ + + + +
+ +
+ +
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/testing/mixins/bc-request/index.html b/testing/mixins/bc-request/index.html new file mode 100644 index 000000000..4b9149167 --- /dev/null +++ b/testing/mixins/bc-request/index.html @@ -0,0 +1,1583 @@ + + + + + + + + + + + + + + + + + + + + + + + + bc.request - BreatheCode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + +

bc.request

+ +
+ + + +
+ + + +
+ + + + + + + + +
+ + + +

+ Request + + +

+ + +
+ + +

Mixin with the purpose of cover all the related with the request

+ + +
+ Source code in breathecode/tests/mixins/breathecode_mixin/request.py +
 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
+71
+72
+73
+74
+75
+76
+77
class Request:
+    """Mixin with the purpose of cover all the related with the request"""
+
+    _parent: APITestCase
+
+    def __init__(self, parent, bc) -> None:
+        self._parent = parent
+        self._bc = bc
+
+    def set_headers(self, **kargs: str) -> None:
+        """
+        Set headers.
+
+        ```py
+        # It set the headers with:
+        #   Academy: 1
+        #   ThingOfImportance: potato
+        self.bc.request.set_headers(academy=1, thing_of_importance='potato')
+        ```
+        """
+        headers = {}
+
+        items = [
+            index for index in kargs
+            if kargs[index] and (isinstance(kargs[index], str) or isinstance(kargs[index], int))
+        ]
+
+        for index in items:
+            headers[f'HTTP_{index.upper()}'] = str(kargs[index])
+
+        self._parent.client.credentials(**headers)
+
+    def authenticate(self, user) -> None:
+        """
+        Forces authentication in a request inside a APITestCase.
+
+        Usage:
+
+        ```py
+        # setup the database
+        model = self.bc.database.create(user=1)
+
+        # that setup the request to use the credential of user passed
+        self.bc.request.authenticate(model.user)
+        ```
+
+        Keywords arguments:
+
+        - user: a instance of user model `breathecode.authenticate.models.User`
+        """
+        self._parent.client.force_authenticate(user=user)
+
+    def manual_authentication(self, user) -> None:
+        """
+        Generate a manual authentication using a token, this method is more slower than `authenticate`.
+
+        ```py
+        # setup the database
+        model = self.bc.database.create(user=1)
+
+        # that setup the request to use the credential with tokens of user passed
+        self.bc.request.manual_authentication(model.user)
+        ```
+
+        Keywords arguments:
+
+        - user: a instance of user model `breathecode.authenticate.models.User`.
+        """
+        from breathecode.authenticate.models import Token
+
+        token = Token.objects.create(user=user)
+        self._parent.client.credentials(HTTP_AUTHORIZATION=f'Token {token.key}')
+
+
+ + + +
+ + + + + + + + + +
+ + + +

+authenticate(user) + +

+ + +
+ +

Forces authentication in a request inside a APITestCase.

+

Usage:

+
# setup the database
+model = self.bc.database.create(user=1)
+
+# that setup the request to use the credential of user passed
+self.bc.request.authenticate(model.user)
+
+

Keywords arguments:

+
    +
  • user: a instance of user model breathecode.authenticate.models.User
  • +
+ +
+ Source code in breathecode/tests/mixins/breathecode_mixin/request.py +
38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
def authenticate(self, user) -> None:
+    """
+    Forces authentication in a request inside a APITestCase.
+
+    Usage:
+
+    ```py
+    # setup the database
+    model = self.bc.database.create(user=1)
+
+    # that setup the request to use the credential of user passed
+    self.bc.request.authenticate(model.user)
+    ```
+
+    Keywords arguments:
+
+    - user: a instance of user model `breathecode.authenticate.models.User`
+    """
+    self._parent.client.force_authenticate(user=user)
+
+
+
+ +
+ +
+ + + +

+manual_authentication(user) + +

+ + +
+ +

Generate a manual authentication using a token, this method is more slower than authenticate.

+
# setup the database
+model = self.bc.database.create(user=1)
+
+# that setup the request to use the credential with tokens of user passed
+self.bc.request.manual_authentication(model.user)
+
+

Keywords arguments:

+
    +
  • user: a instance of user model breathecode.authenticate.models.User.
  • +
+ +
+ Source code in breathecode/tests/mixins/breathecode_mixin/request.py +
58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
+71
+72
+73
+74
+75
+76
+77
def manual_authentication(self, user) -> None:
+    """
+    Generate a manual authentication using a token, this method is more slower than `authenticate`.
+
+    ```py
+    # setup the database
+    model = self.bc.database.create(user=1)
+
+    # that setup the request to use the credential with tokens of user passed
+    self.bc.request.manual_authentication(model.user)
+    ```
+
+    Keywords arguments:
+
+    - user: a instance of user model `breathecode.authenticate.models.User`.
+    """
+    from breathecode.authenticate.models import Token
+
+    token = Token.objects.create(user=user)
+    self._parent.client.credentials(HTTP_AUTHORIZATION=f'Token {token.key}')
+
+
+
+ +
+ +
+ + + +

+set_headers(**kargs) + +

+ + +
+ +

Set headers.

+
# It set the headers with:
+#   Academy: 1
+#   ThingOfImportance: potato
+self.bc.request.set_headers(academy=1, thing_of_importance='potato')
+
+ +
+ Source code in breathecode/tests/mixins/breathecode_mixin/request.py +
15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
def set_headers(self, **kargs: str) -> None:
+    """
+    Set headers.
+
+    ```py
+    # It set the headers with:
+    #   Academy: 1
+    #   ThingOfImportance: potato
+    self.bc.request.set_headers(academy=1, thing_of_importance='potato')
+    ```
+    """
+    headers = {}
+
+    items = [
+        index for index in kargs
+        if kargs[index] and (isinstance(kargs[index], str) or isinstance(kargs[index], int))
+    ]
+
+    for index in items:
+        headers[f'HTTP_{index.upper()}'] = str(kargs[index])
+
+    self._parent.client.credentials(**headers)
+
+
+
+ +
+ + + +
+ +
+ +
+ + + + +
+ +
+ +
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/testing/mixins/bc/index.html b/testing/mixins/bc/index.html new file mode 100644 index 000000000..f7e3aaab3 --- /dev/null +++ b/testing/mixins/bc/index.html @@ -0,0 +1,1567 @@ + + + + + + + + + + + + + + + + + + + + + + + + bc - BreatheCode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + +

bc

+ + +
+ + + +
+ + + +
+ + + + + + + + +
+ + + +

+ Breathecode + + +

+ + +
+

+ Bases: BreathecodeInterface

+ + +

Collection of mixins for testing purposes

+ + +
+ Source code in breathecode/tests/mixins/breathecode_mixin/breathecode.py +
class Breathecode(BreathecodeInterface):
+    """Collection of mixins for testing purposes"""
+
+    cache: Cache
+    random: Random
+    datetime: Datetime
+    request: Request
+    database: Database
+    check: Check
+    format: Format
+    fake: Faker
+    garbage_collector: GarbageCollector
+    _parent: APITestCase
+
+    def __init__(self, parent) -> None:
+        self._parent = parent
+
+        self.cache = Cache(parent, self)
+        self.random = Random(parent, self)
+        self.datetime = Datetime(parent, self)
+        self.request = Request(parent, self)
+        self.database = Database(parent, self)
+        self.check = Check(parent, self)
+        self.format = Format(parent, self)
+        self.garbage_collector = GarbageCollector(parent, self)
+        self.fake = fake
+
+    def help(self, *args) -> None:
+        """
+        Print a list of mixin with a tree style (command of Linux).
+
+        Usage:
+
+        ```py
+        # this print a tree with all the mixins
+        self.bc.help()
+
+        # this print just the docs of corresponding method
+        self.bc.help('bc.datetime.now')
+        ```
+        """
+
+        if args:
+            for arg in args:
+                self._get_doctring(arg)
+
+        else:
+            self._help_tree()
+
+        # prevent left a `self.bc.help()` in the code
+        assert False
+
+    def _get_doctring(self, path: str) -> None:
+        parts_of_path = path.split('.')
+        current_path = ''
+        current = None
+
+        for part_of_path in parts_of_path:
+            if not current:
+                if not hasattr(self._parent, part_of_path):
+                    current_path += f'.{part_of_path}'
+                    break
+
+                current = getattr(self._parent, part_of_path)
+
+            else:
+                if not hasattr(current, part_of_path):
+                    current_path += f'.{part_of_path}'
+                    current = None
+                    break
+
+                current = getattr(current, part_of_path)
+
+        if current:
+            from unittest.mock import patch, MagicMock
+
+            if callable(current):
+                print(f'self.{path}{print_arguments(current)}:')
+            else:
+                print(f'self.{path}:')
+
+            print()
+
+            with patch('sys.stdout.write', MagicMock()) as mock:
+                help(current)
+
+            for args, _ in mock.call_args_list:
+                if args[0] == '\n':
+                    print()
+                lines = args[0].split('\n')
+
+                for line in lines[3:-1]:
+                    print(f'    {line}')
+
+        else:
+            print(f'self.{path}:')
+            print()
+            print(f'    self{current_path} not exists.')
+
+        print()
+
+    def _help_tree(self, level: int = 0, parent: Optional[dict] = None, last_item: bool = False) -> list[str]:
+        """Print a list of mixin with a tree style (command of Linux)"""
+
+        result: list[str] = []
+
+        if not parent:
+            result.append('bc')
+
+        parent = [x for x in dir(parent or self) if not x.startswith('_')]
+
+        if last_item:
+            starts = '    ' + ('│   ' * (level - 1))
+
+        else:
+            starts = '│   ' * level
+
+        for key in parent:
+            item = getattr(self, key)
+
+            if callable(item):
+                result.append(f'{starts}├── {key}{print_arguments(item)}')
+
+            else:
+                result.append(f'{starts}├── {key}')
+
+                last_item = parent.index(key) == len(parent) - 1
+                result = [*result, *Breathecode._help_tree(item, level + 1, item, last_item)]
+
+        result[-1] = result[-1].replace('  ├── ', '  └── ')
+        result[-1] = result[-1].replace(r'├── ([a-zA-Z0-9]+)$', r'└── \1')
+
+        for n in range(len(result) - 1, -1, -1):
+            if result[n][0] == '├':
+                result[n] = re.sub(r'^├', r'└', result[n])
+                break
+
+        if level == 0:
+            print('\n'.join(result))
+
+        return result
+
+
+ + + +
+ + + + + + + + + +
+ + + +

+help(*args) + +

+ + +
+ +

Print a list of mixin with a tree style (command of Linux).

+

Usage:

+
# this print a tree with all the mixins
+self.bc.help()
+
+# this print just the docs of corresponding method
+self.bc.help('bc.datetime.now')
+
+ +
+ Source code in breathecode/tests/mixins/breathecode_mixin/breathecode.py +
58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
+71
+72
+73
+74
+75
+76
+77
+78
+79
+80
+81
def help(self, *args) -> None:
+    """
+    Print a list of mixin with a tree style (command of Linux).
+
+    Usage:
+
+    ```py
+    # this print a tree with all the mixins
+    self.bc.help()
+
+    # this print just the docs of corresponding method
+    self.bc.help('bc.datetime.now')
+    ```
+    """
+
+    if args:
+        for arg in args:
+            self._get_doctring(arg)
+
+    else:
+        self._help_tree()
+
+    # prevent left a `self.bc.help()` in the code
+    assert False
+
+
+
+ +
+ + + +
+ +
+ +
+ + + + +
+ +
+ +
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/testing/mocks/mock-requests/index.html b/testing/mocks/mock-requests/index.html new file mode 100644 index 000000000..a94191be7 --- /dev/null +++ b/testing/mocks/mock-requests/index.html @@ -0,0 +1,1944 @@ + + + + + + + + + + + + + + + + + + + + + + Mock requests - BreatheCode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + +

Mock requests

+ +
+ + + +
+ +

Mocks for requests module

+ + + +
+ + + + + + + + + +
+ + + +

+apply_requests_delete_mock(endpoints=[]) + +

+ + +
+ +

Apply a mock to requests.delete.

+

Usage:

+
import requests
+from unittest.mock import patch, call
+from breathecode.tests.mocks import apply_requests_delete_mock
+from breathecode.is_doesnt_exists import get_eventbrite_description_for_event
+
+@patch('requests.delete', apply_requests_delete_mock([
+    204,
+    'https://www.eventbriteapi.com/v3/events/1/structured_content/',
+    None,
+]))
+def test_xyz():
+    delete_eventbrite_descriptions_for_event(1)
+
+    assert requests.delete.call_args_list == [
+        call('https://www.eventbriteapi.com/v3/events/1/structured_content/',
+            headers={'Authorization': f'Bearer 1234567890'},
+            data=None),
+    ]
+
+ +
+ Source code in breathecode/tests/mocks/requests/__init__.py +
def apply_requests_delete_mock(endpoints=[]):
+    """
+    Apply a mock to `requests.delete`.
+
+    Usage:
+
+    ```py
+    import requests
+    from unittest.mock import patch, call
+    from breathecode.tests.mocks import apply_requests_delete_mock
+    from breathecode.is_doesnt_exists import get_eventbrite_description_for_event
+
+    @patch('requests.delete', apply_requests_delete_mock([
+        204,
+        'https://www.eventbriteapi.com/v3/events/1/structured_content/',
+        None,
+    ]))
+    def test_xyz():
+        delete_eventbrite_descriptions_for_event(1)
+
+        assert requests.delete.call_args_list == [
+            call('https://www.eventbriteapi.com/v3/events/1/structured_content/',
+                headers={'Authorization': f'Bearer 1234567890'},
+                data=None),
+        ]
+    ```
+    """
+    return apply_requests_mock('DELETE', endpoints)
+
+
+
+ +
+ +
+ + + +

+apply_requests_get_mock(endpoints=[]) + +

+ + +
+ +

Apply a mock to requests.get.

+

Usage:

+
import requests
+from unittest.mock import patch, call
+from breathecode.tests.mocks import apply_requests_get_mock
+from breathecode.is_doesnt_exists import get_eventbrite_description_for_event
+
+@patch('requests.get', apply_requests_get_mock([
+    200,
+    'https://www.eventbriteapi.com/v3/events/1/structured_content/',
+    { 'data': { ... } },
+]))
+def test_xyz():
+    get_eventbrite_descriptions_for_event(1)
+
+    assert requests.get.call_args_list == [
+        call('https://www.eventbriteapi.com/v3/events/1/structured_content/',
+            headers={'Authorization': f'Bearer 1234567890'},
+            data=None),
+    ]
+
+ +
+ Source code in breathecode/tests/mocks/requests/__init__.py +
41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
def apply_requests_get_mock(endpoints=[]):
+    """
+    Apply a mock to `requests.get`.
+
+    Usage:
+
+    ```py
+    import requests
+    from unittest.mock import patch, call
+    from breathecode.tests.mocks import apply_requests_get_mock
+    from breathecode.is_doesnt_exists import get_eventbrite_description_for_event
+
+    @patch('requests.get', apply_requests_get_mock([
+        200,
+        'https://www.eventbriteapi.com/v3/events/1/structured_content/',
+        { 'data': { ... } },
+    ]))
+    def test_xyz():
+        get_eventbrite_descriptions_for_event(1)
+
+        assert requests.get.call_args_list == [
+            call('https://www.eventbriteapi.com/v3/events/1/structured_content/',
+                headers={'Authorization': f'Bearer 1234567890'},
+                data=None),
+        ]
+    ```
+    """
+    return apply_requests_mock('GET', endpoints)
+
+
+
+ +
+ +
+ + + +

+apply_requests_head_mock(endpoints=[]) + +

+ + +
+ +

Apply a mock to requests.head.

+

Usage:

+
import requests
+from unittest.mock import patch, call
+from breathecode.tests.mocks import apply_requests_head_mock
+from breathecode.is_doesnt_exists import get_eventbrite_description_for_event
+
+@patch('requests.head', apply_requests_head_mock([
+    200,
+    'https://www.eventbriteapi.com/v3/events/1/structured_content/',
+    None,
+]))
+def test_xyz():
+    get_meta_for_eventbrite_description_for_event(1)
+
+    assert requests.head.call_args_list == [
+        call('https://www.eventbriteapi.com/v3/events/1/structured_content/',
+            headers={'Authorization': f'Bearer 1234567890'},
+            data=None),
+    ]
+
+ +
+ Source code in breathecode/tests/mocks/requests/__init__.py +
def apply_requests_head_mock(endpoints=[]):
+    """
+    Apply a mock to `requests.head`.
+
+    Usage:
+
+    ```py
+    import requests
+    from unittest.mock import patch, call
+    from breathecode.tests.mocks import apply_requests_head_mock
+    from breathecode.is_doesnt_exists import get_eventbrite_description_for_event
+
+    @patch('requests.head', apply_requests_head_mock([
+        200,
+        'https://www.eventbriteapi.com/v3/events/1/structured_content/',
+        None,
+    ]))
+    def test_xyz():
+        get_meta_for_eventbrite_description_for_event(1)
+
+        assert requests.head.call_args_list == [
+            call('https://www.eventbriteapi.com/v3/events/1/structured_content/',
+                headers={'Authorization': f'Bearer 1234567890'},
+                data=None),
+        ]
+    ```
+    """
+    return apply_requests_mock('HEAD', endpoints)
+
+
+
+ +
+ +
+ + + +

+apply_requests_mock(method='get', endpoints=[]) + +

+ + +
+ +

Apply Storage Blob Mock

+ +
+ Source code in breathecode/tests/mocks/requests/__init__.py +
34
+35
+36
+37
+38
def apply_requests_mock(method='get', endpoints=[]):
+    """Apply Storage Blob Mock"""
+    method = method.lower()
+    REQUESTS_INSTANCES[method] = request_mock(endpoints)
+    return REQUESTS_INSTANCES[method]
+
+
+
+ +
+ +
+ + + +

+apply_requests_patch_mock(endpoints=[]) + +

+ + +
+ +

Apply a mock to requests.patch.

+

Usage:

+
import requests
+from unittest.mock import patch, call
+from breathecode.tests.mocks import apply_requests_patch_mock
+from breathecode.is_doesnt_exists import get_eventbrite_description_for_event
+
+@patch('requests.patch', apply_requests_patch_mock([
+    200,
+    'https://www.eventbriteapi.com/v3/events/1/structured_content/',
+    None,
+]))
+def test_xyz():
+    patch_eventbrite_descriptions_for_event(1)
+
+    assert requests.patch.call_args_list == [
+        call('https://www.eventbriteapi.com/v3/events/1/structured_content/',
+            headers={'Authorization': f'Bearer 1234567890'},
+            data=None),
+    ]
+
+ +
+ Source code in breathecode/tests/mocks/requests/__init__.py +
def apply_requests_patch_mock(endpoints=[]):
+    """
+    Apply a mock to `requests.patch`.
+
+    Usage:
+
+    ```py
+    import requests
+    from unittest.mock import patch, call
+    from breathecode.tests.mocks import apply_requests_patch_mock
+    from breathecode.is_doesnt_exists import get_eventbrite_description_for_event
+
+    @patch('requests.patch', apply_requests_patch_mock([
+        200,
+        'https://www.eventbriteapi.com/v3/events/1/structured_content/',
+        None,
+    ]))
+    def test_xyz():
+        patch_eventbrite_descriptions_for_event(1)
+
+        assert requests.patch.call_args_list == [
+            call('https://www.eventbriteapi.com/v3/events/1/structured_content/',
+                headers={'Authorization': f'Bearer 1234567890'},
+                data=None),
+        ]
+    ```
+    """
+    return apply_requests_mock('PATCH', endpoints)
+
+
+
+ +
+ +
+ + + +

+apply_requests_post_mock(endpoints=[]) + +

+ + +
+ +

Apply a mock to requests.post.

+

Usage:

+
import requests
+from unittest.mock import patch, call
+from breathecode.tests.mocks import apply_requests_post_mock
+from breathecode.is_doesnt_exists import get_eventbrite_description_for_event
+
+@patch('requests.post', apply_requests_post_mock([
+    201,
+    'https://www.eventbriteapi.com/v3/events/1/structured_content/',
+    { 'data': { ... } },
+]))
+def test_xyz():
+    post_eventbrite_descriptions_for_event(1)
+
+    assert requests.post.call_args_list == [
+        call('https://www.eventbriteapi.com/v3/events/1/structured_content/',
+            headers={'Authorization': f'Bearer 1234567890'},
+            data=None),
+    ]
+
+ +
+ Source code in breathecode/tests/mocks/requests/__init__.py +
71
+72
+73
+74
+75
+76
+77
+78
+79
+80
+81
+82
+83
+84
+85
+86
+87
+88
+89
+90
+91
+92
+93
+94
+95
+96
+97
+98
def apply_requests_post_mock(endpoints=[]):
+    """
+    Apply a mock to `requests.post`.
+
+    Usage:
+
+    ```py
+    import requests
+    from unittest.mock import patch, call
+    from breathecode.tests.mocks import apply_requests_post_mock
+    from breathecode.is_doesnt_exists import get_eventbrite_description_for_event
+
+    @patch('requests.post', apply_requests_post_mock([
+        201,
+        'https://www.eventbriteapi.com/v3/events/1/structured_content/',
+        { 'data': { ... } },
+    ]))
+    def test_xyz():
+        post_eventbrite_descriptions_for_event(1)
+
+        assert requests.post.call_args_list == [
+            call('https://www.eventbriteapi.com/v3/events/1/structured_content/',
+                headers={'Authorization': f'Bearer 1234567890'},
+                data=None),
+        ]
+    ```
+    """
+    return apply_requests_mock('POST', endpoints)
+
+
+
+ +
+ +
+ + + +

+apply_requests_put_mock(endpoints=[]) + +

+ + +
+ +

Apply a mock to requests.put.

+

Usage:

+
import requests
+from unittest.mock import patch, call
+from breathecode.tests.mocks import apply_requests_put_mock
+from breathecode.is_doesnt_exists import get_eventbrite_description_for_event
+
+@patch('requests.put', apply_requests_put_mock([
+    200,
+    'https://www.eventbriteapi.com/v3/events/1/structured_content/',
+    { 'data': { ... } },
+]))
+def test_xyz():
+    put_eventbrite_descriptions_for_event(1)
+
+    assert requests.put.call_args_list == [
+        call('https://www.eventbriteapi.com/v3/events/1/structured_content/',
+            headers={'Authorization': f'Bearer 1234567890'},
+            data=None),
+    ]
+
+ +
+ Source code in breathecode/tests/mocks/requests/__init__.py +
def apply_requests_put_mock(endpoints=[]):
+    """
+    Apply a mock to `requests.put`.
+
+    Usage:
+
+    ```py
+    import requests
+    from unittest.mock import patch, call
+    from breathecode.tests.mocks import apply_requests_put_mock
+    from breathecode.is_doesnt_exists import get_eventbrite_description_for_event
+
+    @patch('requests.put', apply_requests_put_mock([
+        200,
+        'https://www.eventbriteapi.com/v3/events/1/structured_content/',
+        { 'data': { ... } },
+    ]))
+    def test_xyz():
+        put_eventbrite_descriptions_for_event(1)
+
+        assert requests.put.call_args_list == [
+            call('https://www.eventbriteapi.com/v3/events/1/structured_content/',
+                headers={'Authorization': f'Bearer 1234567890'},
+                data=None),
+        ]
+    ```
+    """
+    return apply_requests_mock('PUT', endpoints)
+
+
+
+ +
+ +
+ + + +

+apply_requests_request_mock(endpoints=[]) + +

+ + +
+ +

Apply a mock to requests.request.

+

Usage:

+
import requests
+from unittest.mock import patch, call
+from breathecode.tests.mocks import apply_requests_request_mock
+from breathecode.is_doesnt_exists import get_eventbrite_description_for_event
+
+@patch('requests.request', apply_requests_request_mock([
+    200,
+    'https://www.eventbriteapi.com/v3/events/1/structured_content/',
+    { 'data': { ... } },
+]))
+def test_xyz():
+    get_eventbrite_description_for_event(1)
+
+    assert requests.request.call_args_list == [
+        call('GET',
+            'https://www.eventbriteapi.com/v3/events/1/structured_content/',
+            headers={'Authorization': f'Bearer 1234567890'},
+            data=None),
+    ]
+
+ +
+ Source code in breathecode/tests/mocks/requests/__init__.py +
def apply_requests_request_mock(endpoints=[]):
+    """
+    Apply a mock to `requests.request`.
+
+    Usage:
+
+    ```py
+    import requests
+    from unittest.mock import patch, call
+    from breathecode.tests.mocks import apply_requests_request_mock
+    from breathecode.is_doesnt_exists import get_eventbrite_description_for_event
+
+    @patch('requests.request', apply_requests_request_mock([
+        200,
+        'https://www.eventbriteapi.com/v3/events/1/structured_content/',
+        { 'data': { ... } },
+    ]))
+    def test_xyz():
+        get_eventbrite_description_for_event(1)
+
+        assert requests.request.call_args_list == [
+            call('GET',
+                'https://www.eventbriteapi.com/v3/events/1/structured_content/',
+                headers={'Authorization': f'Bearer 1234567890'},
+                data=None),
+        ]
+    ```
+    """
+    return apply_requests_mock('REQUEST', endpoints)
+
+
+
+ +
+ + + +
+ +
+ +
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/testing/mocks/using-mocks/index.html b/testing/mocks/using-mocks/index.html new file mode 100644 index 000000000..406dc2495 --- /dev/null +++ b/testing/mocks/using-mocks/index.html @@ -0,0 +1,1229 @@ + + + + + + + + + + + + + + + + + + + + + + + + Using mocks - BreatheCode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + +

Using mocks

+

Mock object

+

Mock objects are simulated objects that mimic the behavior of real objects in controlled ways, most often as part of a software testing initiative. A programmer typically creates a mock object to test the behavior of some other object, in much the same way that a car designer uses a crash test dummy to simulate the dynamic behavior of a human in vehicle impacts.

+

How to apply a automatic mock

+

The most easier way to create a mock

+

The decorator @patch.object is the best option to implement a mock

+
@patch.object(object_class_or_module, 'method_or_function_to_be_mocked', MagicMock())
+
+

This is the code to test

+
# utils.py
+from .actions import shoot_gun, kenny_s_birth, show
+
+def kenny_killer(kenny_id: int) -> None:
+    # get the current kenny
+    kenny = Kenny.objects.filter(id=kenny_id).first()
+
+    # see - South Park - Coon and friends
+    if kenny:
+        shoot_gun(kenny)
+        kenny_number = kenny_s_birth()
+        show(kenny_number)
+
+

This is a example of use of mocks

+
from unittest.mock import MagicMock, call, patch
+from rest_framework.test import APITestCase
+from .models import Kenny
+from .utils import kenny_killer
+
+import app.actions as actions
+
+# this is a wrapper that implement the kenny_s_birth static behavior to the test
+def kenny_s_birth_mock(number: int):
+    def kenny_s_birth():
+        return number
+
+    # the side_effect is a function that manage the behavior of the mocked function
+    return MagicMock(side_effect=kenny_s_birth)
+
+class KennyTestSuite(APITestCase):
+    # 🔽 this function is automatically mocked
+    @patch.object(actions, 'shoot_gun', MagicMock())
+
+    # 🔽 this function is manually mocked
+    @patch.object(actions, 'kenny_s_birth', kenny_s_birth_mock(1000))
+
+    # 🔽 this function is automatically mocked
+    @patch.object(actions, 'show', MagicMock())
+
+    def test_kill_kenny(self):
+        kenny = Kenny()
+        kenny.save()
+
+        kenny_killer(kenny_id=1)
+
+        # shoot_gun() is called with a kenny instance
+        self.assertEqual(actions.shoot_gun.call_args_list, [call(kenny)])
+
+        # kenny_s_birth() is called with zero arguments
+        self.assertEqual(actions.kenny_s_birth.call_args_list, [call()])
+
+        # show is called
+        self.assertEqual(actions.show.call_args_list, [call(1)])
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/testing/runing-tests/index.html b/testing/runing-tests/index.html new file mode 100644 index 000000000..d6013ca8d --- /dev/null +++ b/testing/runing-tests/index.html @@ -0,0 +1,1173 @@ + + + + + + + + + + + + + + + + + + + + + + + + Runing tests - BreatheCode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + +

Runing tests

+

Run tests

+
pipenv run test ./breathecode/
+
+

Run tests in parallel

+
pipenv run ptest ./breathecode/
+
+

Run coverage

+
pipenv run cov breathecode
+
+

Run coverage in parallel

+
pipenv run pcov breathecode
+
+

Testing inside Docker (fallback option)

+
    +
  1. Check which dependencies you need install in you operating system pipenv run doctor or python -m scripts.doctor.
  2. +
  3. Install docker desktop in your Windows, else find a guide to install Docker and Docker Compose in your linux distribution uname -a.
  4. +
  5. Generate the BreatheCode Shell image with pipenv run docker_build_shell.
  6. +
  7. Run BreatheCode Shell with docker-compose run bc-shell
  8. +
  9. Run pipenv run test, pipenv run ptest, pipenv run cov or pipenv run pcov.
  10. +
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file